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