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