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