001/*
002 * (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Olivier Grisel <ogrisel@nuxeo.com>
018 */
019package org.nuxeo.drive.listener;
020
021import static org.nuxeo.ecm.core.trash.TrashService.DOCUMENT_TRASHED;
022import static org.nuxeo.ecm.core.trash.TrashService.DOCUMENT_UNTRASHED;
023
024import java.security.Principal;
025import java.util.ArrayList;
026import java.util.Calendar;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.nuxeo.drive.adapter.FileSystemItem;
035import org.nuxeo.drive.adapter.NuxeoDriveContribException;
036import org.nuxeo.drive.adapter.RootlessItemException;
037import org.nuxeo.drive.service.FileSystemChangeFinder;
038import org.nuxeo.drive.service.FileSystemItemAdapterService;
039import org.nuxeo.drive.service.NuxeoDriveEvents;
040import org.nuxeo.drive.service.impl.NuxeoDriveManagerImpl;
041import org.nuxeo.ecm.core.api.CoreSession;
042import org.nuxeo.ecm.core.api.DocumentModel;
043import org.nuxeo.ecm.core.api.LifeCycleConstants;
044import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
045import org.nuxeo.ecm.core.api.event.CoreEventConstants;
046import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
047import org.nuxeo.ecm.core.event.Event;
048import org.nuxeo.ecm.core.event.EventContext;
049import org.nuxeo.ecm.core.event.EventListener;
050import org.nuxeo.ecm.core.event.EventProducer;
051import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
052import org.nuxeo.ecm.core.event.impl.EventContextImpl;
053import org.nuxeo.ecm.core.query.sql.NXQL;
054import org.nuxeo.ecm.core.schema.FacetNames;
055import org.nuxeo.ecm.platform.audit.api.AuditLogger;
056import org.nuxeo.ecm.platform.audit.api.ExtendedInfo;
057import org.nuxeo.ecm.platform.audit.api.LogEntry;
058import org.nuxeo.runtime.api.Framework;
059
060/**
061 * Synchronous event listener to track events that cannot be directly handled by the {@link FileSystemChangeFinder}
062 * because the document bound to the event is either no more adaptable as a {@link FileSystemItem} after the transaction
063 * has been committed (e.g. deletion) or not a descendant of a synchronization root (e.g. security update on any
064 * document). In particular this includes:
065 * <ul>
066 * <li>Synchronization root unregistration (user specific).</li>
067 * <li>Simple document or synchronization root lifecycle change to the 'deleted' state.</li>
068 * <li>Simple document or synchronization root physical removal from the directory.</li>
069 * <li>Update of a document after which it has no blob.</li>
070 * <li>Move of a document to a non synchronized folder.</li>
071 * <li>Security update.</li>
072 * <li>Group change.</li>
073 * </ul>
074 * <p>
075 * The listener injects virtual entries in the audit logs with the {@link NuxeoDriveEvents#EVENT_CATEGORY} category to
076 * be handled by the {@link FileSystemChangeFinder}. These entries are set in the context of a
077 * {@link NuxeoDriveEvents#VIRTUAL_EVENT_CREATED} event handled by the post-commit asynchronous
078 * {@link NuxeoDriveVirtualEventLogger} to ensure that the transaction is committed before the log entries are actually
079 * added.
080 */
081public class NuxeoDriveFileSystemDeletionListener implements EventListener {
082
083    private static final Log log = LogFactory.getLog(NuxeoDriveFileSystemDeletionListener.class);
084
085    @Override
086    public void handleEvent(Event event) {
087        DocumentEventContext ctx;
088        if (event.getContext() instanceof DocumentEventContext) {
089            ctx = (DocumentEventContext) event.getContext();
090        } else {
091            // Not interested in events that are not related to documents
092            return;
093        }
094        DocumentModel doc = ctx.getSourceDocument();
095        if (doc.hasFacet(FacetNames.SYSTEM_DOCUMENT)) {
096            // Not interested in system documents
097            return;
098        }
099        DocumentModel docForLogEntry = doc;
100        if (DocumentEventTypes.BEFORE_DOC_UPDATE.equals(event.getName())) {
101            docForLogEntry = handleBeforeDocUpdate(ctx, doc);
102            if (docForLogEntry == null) {
103                return;
104            }
105        }
106        if (!DOCUMENT_TRASHED.equals(event.getName())) {
107            // Fallback on the transition event check
108            if (LifeCycleConstants.TRANSITION_EVENT.equals(event.getName()) && !handleLifeCycleTransition(ctx)) {
109                return;
110            }
111        }
112        if (DocumentEventTypes.ABOUT_TO_REMOVE.equals(event.getName()) && !handleAboutToRemove(doc)) {
113            return;
114        }
115        if (log.isDebugEnabled()) {
116            log.debug(String.format("NuxeoDriveFileSystemDeletionListener handling %s event for %s", event.getName(),
117                    doc));
118        }
119        // Virtual event name
120        String virtualEventName;
121        if (DocumentEventTypes.BEFORE_DOC_SECU_UPDATE.equals(event.getName())
122                || NuxeoDriveEvents.GROUP_UPDATED.equals(event.getName())) {
123            virtualEventName = NuxeoDriveEvents.SECURITY_UPDATED_EVENT;
124        } else if (DocumentEventTypes.ABOUT_TO_MOVE.equals(event.getName())) {
125            virtualEventName = NuxeoDriveEvents.MOVED_EVENT;
126        } else {
127            virtualEventName = NuxeoDriveEvents.DELETED_EVENT;
128        }
129        // Some events will only impact a specific user (e.g. root
130        // unregistration)
131        String impactedUserName = (String) ctx.getProperty(NuxeoDriveEvents.IMPACTED_USERNAME_PROPERTY);
132        fireVirtualEventLogEntries(docForLogEntry, virtualEventName, ctx.getPrincipal(), impactedUserName,
133                ctx.getCoreSession());
134    }
135
136    protected DocumentModel handleBeforeDocUpdate(DocumentEventContext ctx, DocumentModel doc) {
137        // Interested in update of a BlobHolder whose blob has been removed
138        boolean blobRemoved = false;
139        DocumentModel previousDoc = (DocumentModel) ctx.getProperty(CoreEventConstants.PREVIOUS_DOCUMENT_MODEL);
140        if (previousDoc != null) {
141            BlobHolder previousBh = previousDoc.getAdapter(BlobHolder.class);
142            if (previousBh != null) {
143                BlobHolder bh = doc.getAdapter(BlobHolder.class);
144                if (bh != null) {
145                    blobRemoved = previousBh.getBlob() != null && bh.getBlob() == null;
146                }
147            }
148        }
149        if (blobRemoved) {
150            // Use previous doc holding a Blob for it to be adaptable as a
151            // FileSystemItem
152            return previousDoc;
153        } else {
154            return null;
155        }
156    }
157
158    protected boolean handleLifeCycleTransition(DocumentEventContext ctx) {
159        String transition = (String) ctx.getProperty(LifeCycleConstants.TRANSTION_EVENT_OPTION_TRANSITION);
160        // Interested in 'deleted' life cycle transition only
161        return transition != null && LifeCycleConstants.DELETE_TRANSITION.equals(transition);
162
163    }
164
165    protected boolean handleAboutToRemove(DocumentModel doc) {
166        // Document deletion of document that are already in the trash should not be marked as FS deletion to avoid
167        // duplicates
168        return !doc.isTrashed();
169    }
170
171    protected void fireVirtualEventLogEntries(DocumentModel doc, String eventName, Principal principal,
172            String impactedUserName, CoreSession session) {
173
174        if (Framework.getService(AuditLogger.class) == null) {
175            // The log is not deployed (probably in unittest)
176            return;
177        }
178
179        List<LogEntry> entries = new ArrayList<>();
180        // XXX: shall we use the server local for the event date or UTC?
181        Date currentDate = Calendar.getInstance(NuxeoDriveManagerImpl.UTC).getTime();
182        FileSystemItem fsItem = getFileSystemItem(doc, eventName);
183        if (fsItem == null) {
184            // NXP-21373: Let's check if we need to propagate the securityUpdated virtual event to child synchronization
185            // roots in order to make Drive add / remove them if needed
186            if (NuxeoDriveEvents.SECURITY_UPDATED_EVENT.equals(eventName)) {
187                for (DocumentModel childSyncRoot : getChildSyncRoots(doc, session)) {
188                    FileSystemItem childSyncRootFSItem = getFileSystemItem(childSyncRoot, eventName);
189                    if (childSyncRootFSItem != null) {
190                        entries.add(computeLogEntry(eventName, currentDate, childSyncRoot.getId(),
191                                childSyncRoot.getPathAsString(), principal.getName(), childSyncRoot.getType(),
192                                childSyncRoot.getRepositoryName(), childSyncRoot.getCurrentLifeCycleState(),
193                                impactedUserName, childSyncRootFSItem));
194                    }
195                }
196            }
197        } else {
198            entries.add(computeLogEntry(eventName, currentDate, doc.getId(), doc.getPathAsString(), principal.getName(),
199                    doc.getType(), doc.getRepositoryName(), doc.getCurrentLifeCycleState(), impactedUserName, fsItem));
200        }
201
202        if (!entries.isEmpty()) {
203            EventContext eventContext = new EventContextImpl(entries.toArray());
204            Event event = eventContext.newEvent(NuxeoDriveEvents.VIRTUAL_EVENT_CREATED);
205            Framework.getService(EventProducer.class).fireEvent(event);
206        }
207    }
208
209    protected FileSystemItem getFileSystemItem(DocumentModel doc, String eventName) {
210        try {
211            // NXP-19442: Avoid useless and costly call to DocumentModel#getLockInfo
212            return Framework.getService(FileSystemItemAdapterService.class).getFileSystemItem(doc, true, true,
213                    false);
214        } catch (RootlessItemException e) {
215            // can happen when deleting a folder under and unregistered root:
216            // nothing to do
217            return null;
218        } catch (NuxeoDriveContribException e) {
219            // Nuxeo Drive contributions missing or component not ready
220            if (log.isDebugEnabled()) {
221                log.debug(String.format(
222                        "Either Nuxeo Drive contributions are missing or the FileSystemItemAdapterService component is not ready (application has nor started yet) => ignoring event '%s'.",
223                        eventName));
224            }
225            return null;
226        }
227    }
228
229    protected List<DocumentModel> getChildSyncRoots(DocumentModel doc, CoreSession session) {
230        String nxql = "SELECT * FROM Document WHERE ecm:mixinType = '" + NuxeoDriveManagerImpl.NUXEO_DRIVE_FACET
231                + "' AND ecm:isTrashed = 0 AND ecm:isVersion = 0 AND ecm:path STARTSWITH "
232                + NXQL.escapeString(doc.getPathAsString());
233        return session.query(nxql);
234    }
235
236    protected LogEntry computeLogEntry(String eventName, Date eventDate, String docId, String docPath, String principal,
237            String docType, String repositoryName, String currentLifeCycleState, String impactedUserName,
238            FileSystemItem fsItem) {
239
240        AuditLogger logger = Framework.getService(AuditLogger.class);
241        LogEntry entry = logger.newLogEntry();
242        entry.setEventId(eventName);
243        entry.setEventDate(eventDate);
244        entry.setCategory(NuxeoDriveEvents.EVENT_CATEGORY);
245        entry.setDocUUID(docId);
246        entry.setDocPath(docPath);
247        entry.setPrincipalName(principal);
248        entry.setDocType(docType);
249        entry.setRepositoryId(repositoryName);
250        entry.setDocLifeCycle(currentLifeCycleState);
251
252        Map<String, ExtendedInfo> extendedInfos = new HashMap<String, ExtendedInfo>();
253        if (impactedUserName != null) {
254            extendedInfos.put("impactedUserName", logger.newExtendedInfo(impactedUserName));
255        }
256        // We do not serialize the whole object as it's too big to fit in a
257        // StringInfo column
258        extendedInfos.put("fileSystemItemId", logger.newExtendedInfo(fsItem.getId()));
259        extendedInfos.put("fileSystemItemName", logger.newExtendedInfo(fsItem.getName()));
260        entry.setExtendedInfos(extendedInfos);
261
262        return entry;
263    }
264
265}