001/* 002 * (C) Copyright 2006-2020 Nuxeo (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 * Nuxeo - initial API and implementation 018 */ 019package org.nuxeo.ecm.core.lifecycle.event; 020 021import static org.apache.commons.lang3.StringUtils.isBlank; 022 023import java.util.List; 024 025import org.apache.commons.logging.Log; 026import org.apache.commons.logging.LogFactory; 027import org.nuxeo.ecm.core.api.CoreSession; 028import org.nuxeo.ecm.core.api.DocumentModel; 029import org.nuxeo.ecm.core.api.DocumentModelList; 030import org.nuxeo.ecm.core.api.LifeCycleConstants; 031import org.nuxeo.ecm.core.api.event.CoreEventConstants; 032import org.nuxeo.ecm.core.api.event.DocumentEventCategories; 033import org.nuxeo.ecm.core.api.event.DocumentEventTypes; 034import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl; 035import org.nuxeo.ecm.core.api.trash.TrashService; 036import org.nuxeo.ecm.core.event.Event; 037import org.nuxeo.ecm.core.event.EventBundle; 038import org.nuxeo.ecm.core.event.EventContext; 039import org.nuxeo.ecm.core.event.EventService; 040import org.nuxeo.ecm.core.event.PostCommitEventListener; 041import org.nuxeo.ecm.core.event.impl.DocumentEventContext; 042import org.nuxeo.ecm.core.lifecycle.LifeCycleService; 043import org.nuxeo.runtime.api.Framework; 044import org.nuxeo.runtime.services.config.ConfigurationService; 045import org.nuxeo.runtime.transaction.TransactionHelper; 046 047/** 048 * Listener for life cycle change events. 049 * <p> 050 * If event occurs on a folder, it will recurse on children to perform the same transition if possible. 051 * <p> 052 * If the transition event is about marking documents as "deleted", and a child cannot perform the transition, it will 053 * be removed. 054 * <p> 055 * Undelete transitions are not processed, but this listener instead looks for a specific documentUndeleted event. This 056 * is because we want to undelete documents (parents) under which we don't want to recurse. 057 * <p> 058 * Reinit document copy lifeCycle (BulkLifeCycleChangeListener is bound to the event documentCreatedByCopy) 059 */ 060public class BulkLifeCycleChangeListener implements PostCommitEventListener { 061 062 /** 063 * @since 8.10-HF05 9.2 064 */ 065 public static final String PAGINATE_GET_CHILDREN_PROPERTY = "nuxeo.bulkLifeCycleChangeListener.paginate-get-children"; 066 067 /** 068 * @since 8.10-HF05 9.2 069 */ 070 public static final String GET_CHILDREN_PAGE_SIZE_PROPERTY = "nuxeo.bulkLifeCycleChangeListener.get-children-page-size"; 071 072 private static final Log log = LogFactory.getLog(BulkLifeCycleChangeListener.class); 073 074 @Override 075 public void handleEvent(EventBundle events) { 076 if (!events.containsEventName(LifeCycleConstants.TRANSITION_EVENT) 077 && !events.containsEventName(LifeCycleConstants.DOCUMENT_UNDELETED) 078 && !events.containsEventName(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY)) { 079 return; 080 } 081 for (Event event : events) { 082 String name = event.getName(); 083 if (LifeCycleConstants.TRANSITION_EVENT.equals(name) || LifeCycleConstants.DOCUMENT_UNDELETED.equals(name) 084 || DocumentEventTypes.DOCUMENT_CREATED_BY_COPY.equals(name)) { 085 processTransition(event); 086 } 087 } 088 } 089 090 protected void processTransition(Event event) { 091 log.debug("Processing lifecycle change in async listener"); 092 EventContext ctx = event.getContext(); 093 if (!(ctx instanceof DocumentEventContext)) { 094 return; 095 } 096 DocumentEventContext docCtx = (DocumentEventContext) ctx; 097 DocumentModel doc = docCtx.getSourceDocument(); 098 if (!doc.isFolder() && !DocumentEventTypes.DOCUMENT_CREATED_BY_COPY.equals(event.getName())) { 099 return; 100 } 101 CoreSession session = docCtx.getCoreSession(); 102 if (session == null) { 103 log.error("Can not process lifeCycle change since session is null"); 104 return; 105 } 106 String transition; 107 String targetState; 108 if (DocumentEventTypes.DOCUMENT_CREATED_BY_COPY.equals(event.getName())) { 109 if (!Boolean.TRUE.equals(event.getContext().getProperties().get(CoreEventConstants.RESET_LIFECYCLE))) { 110 return; 111 } 112 DocumentModelList docs = new DocumentModelListImpl(); 113 docs.add(doc); 114 if (session.exists(doc.getRef())) { 115 reinitDocumentsLifeCyle(session, docs); 116 session.save(); 117 } 118 } else { 119 if (LifeCycleConstants.TRANSITION_EVENT.equals(event.getName())) { 120 transition = (String) docCtx.getProperty(LifeCycleConstants.TRANSTION_EVENT_OPTION_TRANSITION); 121 if (isNonRecursiveTransition(transition, doc.getType())) { 122 // transition should not recurse into children 123 return; 124 } 125 if (LifeCycleConstants.UNDELETE_TRANSITION.equals(transition)) { 126 // not processed (as we can undelete also parents) 127 // a specific event documentUndeleted will be used instead 128 return; 129 } 130 targetState = (String) docCtx.getProperty(LifeCycleConstants.TRANSTION_EVENT_OPTION_TO); 131 } else { // LifeCycleConstants.DOCUMENT_UNDELETED 132 transition = LifeCycleConstants.UNDELETE_TRANSITION; 133 targetState = ""; // unused 134 } 135 changeChildrenState(session, transition, targetState, doc); 136 } 137 } 138 139 protected void reinitDocumentsLifeCyle(CoreSession documentManager, DocumentModelList docs) { 140 for (DocumentModel docMod : docs) { 141 documentManager.reinitLifeCycleState(docMod.getRef()); 142 if (docMod.isFolder()) { 143 DocumentModelList children = documentManager.query(String.format( 144 "SELECT * FROM Document WHERE ecm:isTrashed = 0 AND ecm:parentId = '%s'", docMod.getRef())); 145 reinitDocumentsLifeCyle(documentManager, children); 146 } 147 } 148 } 149 150 protected boolean isNonRecursiveTransition(String transition, String type) { 151 List<String> nonRecursiveTransitions = Framework.getService(LifeCycleService.class) 152 .getNonRecursiveTransitionForDocType(type); 153 return nonRecursiveTransitions.contains(transition); 154 } 155 156 /** 157 * @since 9.2 158 */ 159 protected void changeChildrenState(CoreSession session, String transition, String targetState, DocumentModel doc) { 160 // Check if we need to paginate children fetch 161 ConfigurationService confService = Framework.getService(ConfigurationService.class); 162 boolean paginate = confService.isBooleanTrue(PAGINATE_GET_CHILDREN_PROPERTY); 163 if (paginate) { 164 long pageSize = confService.getLong(GET_CHILDREN_PAGE_SIZE_PROPERTY, 500); 165 // execute a first query to know total size 166 String query = String.format("SELECT * FROM Document where ecm:parentId ='%s'", doc.getId()); 167 DocumentModelList documents = session.query(query, null, pageSize, 0, true); 168 changeDocumentsState(session, transition, targetState, documents); 169 session.save(); 170 // commit the first page 171 TransactionHelper.commitOrRollbackTransaction(); 172 173 // loop on other children 174 long nbChildren = documents.totalSize(); 175 for (long offset = pageSize; offset < nbChildren; offset += pageSize) { 176 long i = offset; 177 // start a new transaction 178 TransactionHelper.runInTransaction(() -> { 179 DocumentModelList docs = session.query(query, null, pageSize, i, false); 180 changeDocumentsState(session, transition, targetState, docs); 181 session.save(); 182 }); 183 } 184 185 // start a new transaction for following 186 TransactionHelper.startTransaction(); 187 } else { 188 DocumentModelList documents = session.getChildren(doc.getRef()); 189 changeDocumentsState(session, transition, targetState, documents); 190 session.save(); 191 } 192 } 193 194 /** 195 * Change doc state. Don't recurse on children as following transition trigger an event which will be handled by 196 * this listener. 197 * 198 * @since 9.2 199 */ 200 protected void changeDocumentsState(CoreSession session, String transition, String targetState, 201 DocumentModelList docs) { 202 TrashService trashService = Framework.getService(TrashService.class); 203 for (DocumentModel doc : docs) { 204 if (doc.getCurrentLifeCycleState() == null) { 205 if (LifeCycleConstants.DELETED_STATE.equals(targetState)) { 206 log.debug("Doc has no lifecycle, deleting ..."); 207 session.removeDocument(doc.getRef()); 208 } 209 } else if (doc.getAllowedStateTransitions().contains(transition) && !doc.isProxy()) { 210 if (LifeCycleConstants.DELETE_TRANSITION.equals(transition)) { 211 // just skip renaming for trash mechanism 212 // here we leverage backward compatibility mechanism in AbstractSession#followTransition 213 doc.putContextData(TrashService.DISABLE_TRASH_RENAMING, Boolean.TRUE); 214 } else if (LifeCycleConstants.UNDELETE_TRANSITION.equals(transition) 215 && trashService.isMangledName(doc.getName())) { 216 // mangled children names need to be explicitely unmangled 217 session.move(doc.getRef(), doc.getParentRef(), trashService.unmangleName(doc)); 218 } 219 doc.followTransition(transition); 220 // handle children if we're handling the technical documentUndeleted event 221 if (LifeCycleConstants.UNDELETE_TRANSITION.equals(transition) && isBlank(targetState) 222 && doc.isFolder()) { 223 DocumentEventContext ctx = new DocumentEventContext(session, session.getPrincipal(), doc); 224 ctx.setCategory(DocumentEventCategories.EVENT_DOCUMENT_CATEGORY); 225 Framework.getService(EventService.class) 226 .fireEvent(ctx.newEvent(LifeCycleConstants.DOCUMENT_UNDELETED)); 227 } 228 } else { 229 if (targetState.equals(doc.getCurrentLifeCycleState())) { 230 log.debug("Document" + doc.getRef() + " is already in the target LifeCycle state"); 231 } else if (LifeCycleConstants.DELETED_STATE.equals(targetState)) { 232 log.debug("Impossible to change state of " + doc.getRef() + " :removing"); 233 session.removeDocument(doc.getRef()); 234 } else { 235 log.debug("Document" + doc.getRef() + " has no transition to the target LifeCycle state"); 236 } 237 } 238 } 239 } 240 241}