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