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.Calendar;
023import java.util.HashMap;
024import java.util.Map;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.nuxeo.drive.adapter.FileSystemItem;
029import org.nuxeo.drive.adapter.NuxeoDriveContribException;
030import org.nuxeo.drive.adapter.RootlessItemException;
031import org.nuxeo.drive.service.FileSystemItemAdapterService;
032import org.nuxeo.drive.service.NuxeoDriveEvents;
033import org.nuxeo.drive.service.impl.NuxeoDriveManagerImpl;
034import org.nuxeo.ecm.core.api.DocumentModel;
035import org.nuxeo.ecm.core.api.LifeCycleConstants;
036import org.nuxeo.ecm.core.api.blobholder.BlobHolder;
037import org.nuxeo.ecm.core.api.event.CoreEventConstants;
038import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
039import org.nuxeo.ecm.core.event.Event;
040import org.nuxeo.ecm.core.event.EventContext;
041import org.nuxeo.ecm.core.event.EventListener;
042import org.nuxeo.ecm.core.event.EventProducer;
043import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
044import org.nuxeo.ecm.core.event.impl.EventContextImpl;
045import org.nuxeo.ecm.core.schema.FacetNames;
046import org.nuxeo.ecm.platform.audit.api.AuditLogger;
047import org.nuxeo.ecm.platform.audit.api.ExtendedInfo;
048import org.nuxeo.ecm.platform.audit.api.LogEntry;
049import org.nuxeo.runtime.api.Framework;
050
051/**
052 * Event listener to track events that should be mapped to file system item deletions in the the ChangeSummary
053 * computation. In particular this includes <li>Synchronization root unregistration (user specific)</li> <li>Simple
054 * document or root document lifecycle change to the 'deleted' state</li> <li>Simple document or root physical removal
055 * from the directory.</li>
056 */
057public class NuxeoDriveFileSystemDeletionListener implements EventListener {
058
059    private static final Log log = LogFactory.getLog(NuxeoDriveFileSystemDeletionListener.class);
060
061    @Override
062    public void handleEvent(Event event) {
063        DocumentEventContext ctx;
064        if (event.getContext() instanceof DocumentEventContext) {
065            ctx = (DocumentEventContext) event.getContext();
066        } else {
067            // Not interested in events that are not related to documents
068            return;
069        }
070        DocumentModel doc = ctx.getSourceDocument();
071        if (doc.hasFacet(FacetNames.SYSTEM_DOCUMENT)) {
072            // Not interested in system documents
073            return;
074        }
075        DocumentModel docForLogEntry = doc;
076        if (DocumentEventTypes.BEFORE_DOC_UPDATE.equals(event.getName())) {
077            docForLogEntry = handleBeforeDocUpdate(ctx, doc);
078            if (docForLogEntry == null) {
079                return;
080            }
081        }
082        if (LifeCycleConstants.TRANSITION_EVENT.equals(event.getName()) && !handleLifeCycleTransition(ctx)) {
083            return;
084        }
085        if (DocumentEventTypes.ABOUT_TO_REMOVE.equals(event.getName()) && !handleAboutToRemove(doc)) {
086            return;
087        }
088        // Virtual event name
089        String virtualEventName;
090        if (DocumentEventTypes.BEFORE_DOC_SECU_UPDATE.equals(event.getName())) {
091            virtualEventName = NuxeoDriveEvents.SECURITY_UPDATED_EVENT;
092        } else if (DocumentEventTypes.ABOUT_TO_MOVE.equals(event.getName())) {
093            virtualEventName = NuxeoDriveEvents.MOVED_EVENT;
094        } else {
095            virtualEventName = NuxeoDriveEvents.DELETED_EVENT;
096        }
097        // Some events will only impact a specific user (e.g. root
098        // unregistration)
099        String impactedUserName = (String) ctx.getProperty(NuxeoDriveEvents.IMPACTED_USERNAME_PROPERTY);
100        fireVirtualEventLogEntry(docForLogEntry, virtualEventName, ctx.getPrincipal(), impactedUserName);
101    }
102
103    protected DocumentModel handleBeforeDocUpdate(DocumentEventContext ctx, DocumentModel doc) {
104        // Interested in update of a BlobHolder whose blob has been removed
105        boolean blobRemoved = false;
106        DocumentModel previousDoc = (DocumentModel) ctx.getProperty(CoreEventConstants.PREVIOUS_DOCUMENT_MODEL);
107        if (previousDoc != null) {
108            BlobHolder previousBh = previousDoc.getAdapter(BlobHolder.class);
109            if (previousBh != null) {
110                BlobHolder bh = doc.getAdapter(BlobHolder.class);
111                if (bh != null) {
112                    blobRemoved = previousBh.getBlob() != null && bh.getBlob() == null;
113                }
114            }
115        }
116        if (blobRemoved) {
117            // Use previous doc holding a Blob for it to be adaptable as a
118            // FileSystemItem
119            return previousDoc;
120        } else {
121            return null;
122        }
123    }
124
125    protected boolean handleLifeCycleTransition(DocumentEventContext ctx) {
126        String transition = (String) ctx.getProperty(LifeCycleConstants.TRANSTION_EVENT_OPTION_TRANSITION);
127        // Interested in 'deleted' life cycle transition only
128        return transition != null && LifeCycleConstants.DELETE_TRANSITION.equals(transition);
129
130    }
131
132    protected boolean handleAboutToRemove(DocumentModel doc) {
133        // Document deletion of document that are already in deleted
134        // state should not be marked as FS deletion to avoid duplicates
135        return !LifeCycleConstants.DELETED_STATE.equals(doc.getCurrentLifeCycleState());
136    }
137
138    protected void fireVirtualEventLogEntry(DocumentModel doc, String eventName, Principal principal,
139            String impactedUserName) {
140
141        AuditLogger logger = Framework.getLocalService(AuditLogger.class);
142        if (logger == null) {
143            // The log is not deployed (probably in unittest)
144            return;
145        }
146        FileSystemItem fsItem = null;
147        try {
148            // NXP-19442: Avoid useless and costly call to DocumentModel#getLockInfo
149            fsItem = Framework.getLocalService(FileSystemItemAdapterService.class).getFileSystemItem(doc, true, true,
150                    false);
151        } catch (RootlessItemException e) {
152            // can happen when deleting a folder under and unregistered root:
153            // nothing to do
154            return;
155        } catch (NuxeoDriveContribException e) {
156            // Nuxeo Drive contributions missing or component not ready
157            if (log.isDebugEnabled()) {
158                log.debug(String.format(
159                        "Either Nuxeo Drive contributions are missing or the FileSystemItemAdapterService component is not ready (application has nor started yet) => ignoring event '%s'.",
160                        eventName));
161            }
162            return;
163        }
164        if (fsItem == null) {
165            return;
166        }
167
168        LogEntry entry = logger.newLogEntry();
169        entry.setEventId(eventName);
170        // XXX: shall we use the server local for the event date or UTC?
171        entry.setEventDate(Calendar.getInstance(NuxeoDriveManagerImpl.UTC).getTime());
172        entry.setCategory((String) NuxeoDriveEvents.EVENT_CATEGORY);
173        entry.setDocUUID(doc.getId());
174        entry.setDocPath(doc.getPathAsString());
175        entry.setPrincipalName(principal.getName());
176        entry.setDocType(doc.getType());
177        entry.setRepositoryId(doc.getRepositoryName());
178        entry.setDocLifeCycle(doc.getCurrentLifeCycleState());
179
180        Map<String, ExtendedInfo> extendedInfos = new HashMap<String, ExtendedInfo>();
181        if (impactedUserName != null) {
182            extendedInfos.put("impactedUserName", logger.newExtendedInfo(impactedUserName));
183        }
184        // We do not serialize the whole object as it's too big to fit in a
185        // StringInfo column and
186        extendedInfos.put("fileSystemItemId", logger.newExtendedInfo(fsItem.getId()));
187        extendedInfos.put("fileSystemItemName", logger.newExtendedInfo(fsItem.getName()));
188        entry.setExtendedInfos(extendedInfos);
189
190        EventContext eventContext = new EventContextImpl(entry);
191        Event event = eventContext.newEvent(NuxeoDriveEvents.VIRTUAL_EVENT_CREATED);
192        Framework.getService(EventProducer.class).fireEvent(event);
193    }
194
195}