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