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