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}