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