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