001/*
002 * (C) Copyright 2009-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 *     Alexandre Russel
018 *     Florent Guillaume
019 */
020package org.nuxeo.ecm.platform.routing.core.impl;
021
022import static org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider.CORE_SESSION_PROPERTY;
023import static org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider.MAX_RESULTS_PROPERTY;
024import static org.nuxeo.ecm.platform.query.nxql.CoreQueryDocumentPageProvider.PAGE_SIZE_RESULTS_KEY;
025import static org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants.DOC_ROUTING_SEARCH_ALL_ROUTE_MODELS_PROVIDER_NAME;
026import static org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants.DOC_ROUTING_SEARCH_ROUTE_MODELS_WITH_TITLE_PROVIDER_NAME;
027
028import java.io.IOException;
029import java.io.Serializable;
030import java.net.MalformedURLException;
031import java.net.URL;
032import java.util.ArrayList;
033import java.util.Collections;
034import java.util.HashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.concurrent.TimeUnit;
038
039import org.apache.commons.lang.StringUtils;
040import org.apache.commons.logging.Log;
041import org.apache.commons.logging.LogFactory;
042import org.nuxeo.ecm.core.api.Blob;
043import org.nuxeo.ecm.core.api.CoreSession;
044import org.nuxeo.ecm.core.api.DocumentModel;
045import org.nuxeo.ecm.core.api.DocumentModelList;
046import org.nuxeo.ecm.core.api.DocumentNotFoundException;
047import org.nuxeo.ecm.core.api.DocumentRef;
048import org.nuxeo.ecm.core.api.IdRef;
049import org.nuxeo.ecm.core.api.IterableQueryResult;
050import org.nuxeo.ecm.core.api.LifeCycleConstants;
051import org.nuxeo.ecm.core.api.NuxeoGroup;
052import org.nuxeo.ecm.core.api.NuxeoException;
053import org.nuxeo.ecm.core.api.NuxeoPrincipal;
054import org.nuxeo.ecm.core.api.PropertyException;
055import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
056import org.nuxeo.ecm.core.api.impl.blob.URLBlob;
057import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
058import org.nuxeo.ecm.core.api.security.ACE;
059import org.nuxeo.ecm.core.api.security.ACL;
060import org.nuxeo.ecm.core.api.security.ACP;
061import org.nuxeo.ecm.core.api.security.SecurityConstants;
062import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
063import org.nuxeo.ecm.core.event.EventProducer;
064import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
065import org.nuxeo.ecm.core.query.sql.NXQL;
066import org.nuxeo.ecm.core.repository.RepositoryInitializationHandler;
067import org.nuxeo.ecm.platform.filemanager.api.FileManager;
068import org.nuxeo.ecm.platform.query.api.PageProvider;
069import org.nuxeo.ecm.platform.query.api.PageProviderService;
070import org.nuxeo.ecm.platform.routing.api.DocumentRoute;
071import org.nuxeo.ecm.platform.routing.api.DocumentRouteElement;
072import org.nuxeo.ecm.platform.routing.api.DocumentRouteTableElement;
073import org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants;
074import org.nuxeo.ecm.platform.routing.api.DocumentRoutingPersister;
075import org.nuxeo.ecm.platform.routing.api.DocumentRoutingService;
076import org.nuxeo.ecm.platform.routing.api.LockableDocumentRoute;
077import org.nuxeo.ecm.platform.routing.api.RouteFolderElement;
078import org.nuxeo.ecm.platform.routing.api.RouteModelResourceType;
079import org.nuxeo.ecm.platform.routing.api.RouteTable;
080import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteAlredayLockedException;
081import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteException;
082import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteNotLockedException;
083import org.nuxeo.ecm.platform.routing.core.api.DocumentRoutingEngineService;
084import org.nuxeo.ecm.platform.routing.core.audit.RoutingAuditHelper;
085import org.nuxeo.ecm.platform.routing.core.listener.RouteModelsInitializator;
086import org.nuxeo.ecm.platform.routing.core.registries.RouteTemplateResourceRegistry;
087import org.nuxeo.ecm.platform.task.Task;
088import org.nuxeo.ecm.platform.task.TaskConstants;
089import org.nuxeo.ecm.platform.task.TaskEventNames;
090import org.nuxeo.ecm.platform.task.TaskService;
091import org.nuxeo.ecm.platform.task.core.service.TaskEventNotificationHelper;
092import org.nuxeo.runtime.api.Framework;
093import org.nuxeo.runtime.model.ComponentContext;
094import org.nuxeo.runtime.model.ComponentInstance;
095import org.nuxeo.runtime.model.DefaultComponent;
096import org.nuxeo.runtime.model.RuntimeContext;
097
098import com.google.common.cache.Cache;
099import com.google.common.cache.CacheBuilder;
100
101/**
102 * The implementation of the routing service.
103 */
104public class DocumentRoutingServiceImpl extends DefaultComponent implements DocumentRoutingService {
105
106    private static Log log = LogFactory.getLog(DocumentRoutingServiceImpl.class);
107
108    /** Routes in any state (model or not). */
109    private static final String AVAILABLE_ROUTES_QUERY = String.format("SELECT * FROM %s",
110            DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE);
111
112    /** Routes Models. */
113    private static final String AVAILABLE_ROUTES_MODEL_QUERY = String.format(
114            "SELECT * FROM %s WHERE ecm:currentLifeCycleState = '%s'",
115            DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE,
116            DocumentRoutingConstants.DOCUMENT_ROUTE_MODEL_LIFECYCLESTATE);
117
118    /** Route models that have been validated. */
119    private static final String ROUTE_MODEL_WITH_ID_QUERY = String.format(
120            "SELECT * FROM %s WHERE ecm:name = %%s AND ecm:currentLifeCycleState = 'validated' AND ecm:isCheckedInVersion  = 0  AND ecm:isProxy = 0 ",
121            DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE);
122
123    /** Route models that have been validated. */
124    private static final String ROUTE_MODEL_DOC_ID_WITH_ID_QUERY = String.format(
125            "SELECT ecm:uuid FROM %s WHERE ecm:name = %%s AND ecm:currentLifeCycleState = 'validated' AND ecm:isCheckedInVersion  = 0  AND ecm:isProxy = 0 ",
126            DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE);
127
128    private static final String ORDERED_CHILDREN_QUERY = "SELECT * FROM Document WHERE"
129            + " ecm:parentId = '%s' AND ecm:isCheckedInVersion  = 0 AND "
130            + "ecm:currentLifeCycleState != 'deleted' ORDER BY ecm:pos";
131
132    public static final String CHAINS_TO_TYPE_XP = "chainsToType";
133
134    public static final String PERSISTER_XP = "persister";
135
136    /**
137     * @since 7.10
138     */
139    public static final String ACTOR_ACE_CREATOR = "Workflow";
140
141    // FIXME: use ContributionFragmentRegistry instances instead to handle hot
142    // reload
143
144    public static final String ROUTE_MODELS_IMPORTER_XP = "routeModelImporter";
145
146    protected Map<String, String> typeToChain = new HashMap<>();
147
148    protected Map<String, String> undoChainIdFromRunning = new HashMap<>();
149
150    protected Map<String, String> undoChainIdFromDone = new HashMap<>();
151
152    protected DocumentRoutingPersister persister;
153
154    protected RouteTemplateResourceRegistry routeResourcesRegistry = new RouteTemplateResourceRegistry();
155
156    protected RepositoryInitializationHandler repositoryInitializationHandler;
157
158    private Cache<String, String> modelsChache;
159
160    @Override
161    public void registerContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
162        if (CHAINS_TO_TYPE_XP.equals(extensionPoint)) {
163            ChainToTypeMappingDescriptor desc = (ChainToTypeMappingDescriptor) contribution;
164            typeToChain.put(desc.getDocumentType(), desc.getChainId());
165            undoChainIdFromRunning.put(desc.getDocumentType(), desc.getUndoChainIdFromRunning());
166            undoChainIdFromDone.put(desc.getDocumentType(), desc.getUndoChainIdFromDone());
167        } else if (PERSISTER_XP.equals(extensionPoint)) {
168            PersisterDescriptor des = (PersisterDescriptor) contribution;
169            try {
170                persister = des.getKlass().newInstance();
171            } catch (ReflectiveOperationException e) {
172                throw new RuntimeException(e);
173            }
174        } else if (ROUTE_MODELS_IMPORTER_XP.equals(extensionPoint)) {
175            RouteModelResourceType res = (RouteModelResourceType) contribution;
176            registerRouteResource(res, contributor.getRuntimeContext());
177        }
178    }
179
180    @Override
181    public void unregisterContribution(Object contribution, String extensionPoint, ComponentInstance contributor) {
182        if (contribution instanceof RouteModelResourceType) {
183            routeResourcesRegistry.removeContribution((RouteModelResourceType) contribution);
184        }
185        super.unregisterContribution(contribution, extensionPoint, contributor);
186    }
187
188    protected static void fireEvent(String eventName, Map<String, Serializable> eventProperties, DocumentRoute route,
189            CoreSession session) {
190        eventProperties.put(DocumentRoutingConstants.DOCUMENT_ELEMENT_EVENT_CONTEXT_KEY, route);
191        eventProperties.put(DocumentEventContext.CATEGORY_PROPERTY_KEY, DocumentRoutingConstants.ROUTING_CATEGORY);
192        DocumentEventContext envContext = new DocumentEventContext(session, session.getPrincipal(), route.getDocument());
193        envContext.setProperties(eventProperties);
194        EventProducer eventProducer = Framework.getLocalService(EventProducer.class);
195        eventProducer.fireEvent(envContext.newEvent(eventName));
196    }
197
198    @Override
199    public String createNewInstance(final String routeModelId, final List<String> docIds,
200            final Map<String, Serializable> map, CoreSession session, final boolean startInstance) {
201        final String initiator = session.getPrincipal().getName();
202        final String res[] = new String[1];
203        new UnrestrictedSessionRunner(session) {
204
205            protected DocumentRoute route;
206
207            @Override
208            public void run() {
209                String routeDocId = getRouteModelDocIdWithId(session, routeModelId);
210                DocumentModel model = session.getDocument(new IdRef(routeDocId));
211                DocumentModel instance = persister.createDocumentRouteInstanceFromDocumentRouteModel(model, session);
212                route = instance.getAdapter(DocumentRoute.class);
213                route.setAttachedDocuments(docIds);
214                route.save(session);
215                Map<String, Serializable> props = new HashMap<>();
216                props.put(DocumentRoutingConstants.INITIATOR_EVENT_CONTEXT_KEY, initiator);
217                fireEvent(DocumentRoutingConstants.Events.beforeRouteReady.name(), props);
218                route.setReady(session);
219                fireEvent(DocumentRoutingConstants.Events.afterRouteReady.name(), props);
220                route.save(session);
221                if (startInstance) {
222                    fireEvent(DocumentRoutingConstants.Events.beforeRouteStart.name(), new HashMap<>());
223                    DocumentRoutingEngineService routingEngine = Framework.getLocalService(DocumentRoutingEngineService.class);
224                    routingEngine.start(route, map, session);
225                    fireEventAfterWorkflowStarted(route, session);
226                }
227                res[0] = instance.getId();
228            }
229
230            protected void fireEvent(String eventName, Map<String, Serializable> eventProperties) {
231                DocumentRoutingServiceImpl.fireEvent(eventName, eventProperties, route, session);
232            }
233
234        }.runUnrestricted();
235
236        return res[0];
237    }
238
239    @Override
240    public String createNewInstance(String routeModelId, List<String> docIds, CoreSession session, boolean startInstance) {
241        return createNewInstance(routeModelId, docIds, null, session, startInstance);
242    }
243
244    @Override
245    public DocumentRoute createNewInstance(DocumentRoute model, List<String> docIds, CoreSession session,
246            boolean startInstance) {
247        String id = createNewInstance(model.getDocument().getName(), docIds, session, startInstance);
248        return session.getDocument(new IdRef(id)).getAdapter(DocumentRoute.class);
249    }
250
251    @Override
252    @Deprecated
253    public DocumentRoute createNewInstance(DocumentRoute model, String documentId, CoreSession session,
254            boolean startInstance) {
255        return createNewInstance(model, Collections.singletonList(documentId), session, startInstance);
256    }
257
258    @Override
259    @Deprecated
260    public DocumentRoute createNewInstance(DocumentRoute model, List<String> documentIds, CoreSession session) {
261        return createNewInstance(model, documentIds, session, true);
262    }
263
264    @Override
265    @Deprecated
266    public DocumentRoute createNewInstance(DocumentRoute model, String documentId, CoreSession session) {
267        return createNewInstance(model, Collections.singletonList(documentId), session, true);
268    }
269
270    @Override
271    public void startInstance(final String routeInstanceId, final List<String> docIds,
272            final Map<String, Serializable> map, CoreSession session) {
273        new UnrestrictedSessionRunner(session) {
274            @Override
275            public void run() {
276                DocumentModel instance = session.getDocument(new IdRef(routeInstanceId));
277                DocumentRoute route = instance.getAdapter(DocumentRoute.class);
278                if (docIds != null) {
279                    route.setAttachedDocuments(docIds);
280                    route.save(session);
281                }
282                fireEvent(DocumentRoutingConstants.Events.beforeRouteStart.name(), new HashMap<>(), route, session);
283                DocumentRoutingEngineService routingEngine = Framework.getLocalService(DocumentRoutingEngineService.class);
284                routingEngine.start(route, map, session);
285                fireEventAfterWorkflowStarted(route, session);
286            }
287
288        }.runUnrestricted();
289    }
290
291    protected void fireEventAfterWorkflowStarted(DocumentRoute route, CoreSession session) {
292        Map<String, Serializable> eventProperties = new HashMap<>();
293        eventProperties.put(RoutingAuditHelper.WORKFLOW_INITATIOR, route.getInitiator());
294        eventProperties.put("modelId", route.getModelId());
295        eventProperties.put("modelName", route.getModelName());
296        if (route instanceof GraphRoute) {
297            eventProperties.put(RoutingAuditHelper.WORKFLOW_VARIABLES,
298                    (Serializable) ((GraphRoute) route).getVariables());
299        }
300        fireEvent(DocumentRoutingConstants.Events.afterWorkflowStarted.name(), eventProperties, route, session);
301    }
302
303    @Override
304    public void resumeInstance(String routeId, String nodeId, Map<String, Object> data, String status,
305            CoreSession session) {
306        completeTask(routeId, nodeId, null, data, status, session);
307    }
308
309    @Override
310    public void completeTask(String routeId, String taskId, Map<String, Object> data, String status, CoreSession session) {
311        DocumentModel task = session.getDocument(new IdRef(taskId));
312        completeTask(routeId, null, task != null ? task.getAdapter(Task.class) : null, data, status, session);
313    }
314
315    protected void completeTask(final String routeId, final String nodeId, final Task task,
316            final Map<String, Object> data, final String status, CoreSession session) {
317        CompleteTaskRunner runner = new CompleteTaskRunner(routeId, nodeId, task, data, status, session);
318        runner.runUnrestricted();
319    }
320
321    /**
322     * @since 7.4
323     */
324    private class CompleteTaskRunner extends UnrestrictedSessionRunner {
325
326        String routeId;
327
328        String nodeId;
329
330        Task task;
331
332        Map<String, Object> data;
333
334        String status;
335
336        protected CompleteTaskRunner(final String routeId, final String nodeId, final Task task,
337                final Map<String, Object> data, final String status, CoreSession session) {
338            super(session);
339            this.routeId = routeId;
340            this.nodeId = nodeId;
341            this.task = task;
342            this.data = data;
343            this.status = status;
344        }
345
346        @Override
347        public void run() {
348            DocumentRoutingEngineService routingEngine = Framework.getLocalService(DocumentRoutingEngineService.class);
349            DocumentModel routeDoc = session.getDocument(new IdRef(routeId));
350            DocumentRoute routeInstance = routeDoc.getAdapter(DocumentRoute.class);
351            routingEngine.resume(routeInstance, nodeId, task != null ? task.getId() : null, data, status, session);
352        }
353
354    }
355
356    @Override
357    public List<DocumentRoute> getAvailableDocumentRouteModel(CoreSession session) {
358        DocumentModelList list = session.query(AVAILABLE_ROUTES_MODEL_QUERY);
359        List<DocumentRoute> routes = new ArrayList<>();
360        for (DocumentModel model : list) {
361            routes.add(model.getAdapter(DocumentRoute.class));
362        }
363        return routes;
364    }
365
366    @Override
367    public List<DocumentRoute> getAvailableDocumentRoute(CoreSession session) {
368        DocumentModelList list = session.query(AVAILABLE_ROUTES_QUERY);
369        List<DocumentRoute> routes = new ArrayList<>();
370        for (DocumentModel model : list) {
371            routes.add(model.getAdapter(DocumentRoute.class));
372        }
373        return routes;
374    }
375
376    @Override
377    public String getOperationChainId(String documentType) {
378        return typeToChain.get(documentType);
379    }
380
381    @Override
382    public String getUndoFromRunningOperationChainId(String documentType) {
383        return undoChainIdFromRunning.get(documentType);
384    }
385
386    @Override
387    public String getUndoFromDoneOperationChainId(String documentType) {
388        return undoChainIdFromDone.get(documentType);
389    }
390
391    @Override
392    public DocumentRoute unlockDocumentRouteUnrestrictedSession(final DocumentRoute routeModel, CoreSession userSession) {
393        new UnrestrictedSessionRunner(userSession) {
394            @Override
395            public void run() {
396                DocumentRoute route = session.getDocument(routeModel.getDocument().getRef()).getAdapter(
397                        DocumentRoute.class);
398                LockableDocumentRoute lockableRoute = route.getDocument().getAdapter(LockableDocumentRoute.class);
399                lockableRoute.unlockDocument(session);
400            }
401        }.runUnrestricted();
402        return userSession.getDocument(routeModel.getDocument().getRef()).getAdapter(DocumentRoute.class);
403    }
404
405    @Override
406    public DocumentRoute validateRouteModel(final DocumentRoute routeModel, CoreSession userSession)
407            throws DocumentRouteNotLockedException {
408        if (!routeModel.getDocument().isLocked()) {
409            throw new DocumentRouteNotLockedException();
410        }
411        new UnrestrictedSessionRunner(userSession) {
412            @Override
413            public void run() {
414                DocumentRoute route = session.getDocument(routeModel.getDocument().getRef()).getAdapter(
415                        DocumentRoute.class);
416                route.validate(session);
417            }
418        }.runUnrestricted();
419        return userSession.getDocument(routeModel.getDocument().getRef()).getAdapter(DocumentRoute.class);
420    }
421
422    /**
423     * @deprecated since 5.9.2 - Use only routes of type 'graph'
424     */
425    @Deprecated
426    @Override
427    public List<DocumentRouteTableElement> getRouteElements(DocumentRoute route, CoreSession session) {
428        RouteTable table = new RouteTable(route);
429        List<DocumentRouteTableElement> elements = new ArrayList<>();
430        processElementsInFolder(route.getDocument(), elements, table, session, 0, null);
431        int maxDepth = 0;
432        for (DocumentRouteTableElement element : elements) {
433            int d = element.getDepth();
434            maxDepth = d > maxDepth ? d : maxDepth;
435        }
436        table.setMaxDepth(maxDepth);
437        for (DocumentRouteTableElement element : elements) {
438            element.computeFirstChildList();
439        }
440        return elements;
441    }
442
443    /**
444     * @deprecated since 5.9.2 - Use only routes of type 'graph'
445     */
446    @Deprecated
447    protected void processElementsInFolder(DocumentModel doc, List<DocumentRouteTableElement> elements,
448            RouteTable table, CoreSession session, int depth, RouteFolderElement folder) {
449        DocumentModelList children = session.getChildren(doc.getRef());
450        boolean first = true;
451        for (DocumentModel child : children) {
452            if (child.isFolder() && !session.getChildren(child.getRef()).isEmpty()) {
453                RouteFolderElement thisFolder = new RouteFolderElement(child.getAdapter(DocumentRouteElement.class),
454                        table, first, folder, depth);
455                processElementsInFolder(child, elements, table, session, depth + 1, thisFolder);
456            } else {
457                if (folder != null) {
458                    folder.increaseTotalChildCount();
459                } else {
460                    table.increaseTotalChildCount();
461                }
462                elements.add(new DocumentRouteTableElement(child.getAdapter(DocumentRouteElement.class), table, depth,
463                        folder, first));
464            }
465            first = false;
466        }
467    }
468
469    @Deprecated
470    protected List<DocumentRouteTableElement> getRouteElements(DocumentRouteElement routeElementDocument,
471            CoreSession session, List<DocumentRouteTableElement> routeElements, int depth) {
472        return null;
473    }
474
475    @Override
476    public List<DocumentRoute> getDocumentRoutesForAttachedDocument(CoreSession session, String attachedDocId) {
477        List<DocumentRouteElement.ElementLifeCycleState> states = new ArrayList<>();
478        states.add(DocumentRouteElement.ElementLifeCycleState.ready);
479        states.add(DocumentRouteElement.ElementLifeCycleState.running);
480        return getDocumentRoutesForAttachedDocument(session, attachedDocId, states);
481    }
482
483    @Override
484    public List<DocumentRoute> getDocumentRoutesForAttachedDocument(CoreSession session, String attachedDocId,
485            List<DocumentRouteElement.ElementLifeCycleState> states) {
486        DocumentModelList list;
487        StringBuilder statesString = new StringBuilder();
488        if (states != null && !states.isEmpty()) {
489            statesString.append(" ecm:currentLifeCycleState IN (");
490            for (DocumentRouteElement.ElementLifeCycleState state : states) {
491                statesString.append("'" + state.name() + "',");
492            }
493            statesString.deleteCharAt(statesString.length() - 1);
494            statesString.append(") AND");
495        }
496        String query = String.format("SELECT * FROM DocumentRoute WHERE " + statesString.toString()
497                + " docri:participatingDocuments/* = '%s'"
498                // ordering by dc:created makes sure that
499                // a sub-workflow is listed under its parent
500                + " ORDER BY dc:created", attachedDocId);
501        UnrestrictedQueryRunner queryRunner = new UnrestrictedQueryRunner(session, query);
502        list = queryRunner.runQuery();
503        List<DocumentRoute> routes = new ArrayList<>();
504        for (DocumentModel model : list) {
505            routes.add(model.getAdapter(DocumentRoute.class));
506        }
507        return routes;
508    }
509
510    @Override
511    public boolean canUserValidateRoute(NuxeoPrincipal currentUser) {
512        return currentUser.getGroups().contains(DocumentRoutingConstants.ROUTE_MANAGERS_GROUP_NAME);
513    }
514
515    @Override
516    public boolean canValidateRoute(DocumentModel documentRoute, CoreSession coreSession) {
517        if (!coreSession.hasChildren(documentRoute.getRef())) {
518            // Cannot validate an empty route
519            return false;
520        }
521        return coreSession.hasPermission(documentRoute.getRef(), SecurityConstants.EVERYTHING);
522    }
523
524    // @deprecated since 5.9.2 - Use only routes of type 'graph'
525    @Override
526    @Deprecated
527    public void addRouteElementToRoute(DocumentRef parentDocumentRef, int idx, DocumentRouteElement routeElement,
528            CoreSession session) throws DocumentRouteNotLockedException {
529        DocumentRoute route = getParentRouteModel(parentDocumentRef, session);
530        if (!isLockedByCurrentUser(route, session)) {
531            throw new DocumentRouteNotLockedException();
532        }
533        DocumentModelList children = session.query(String.format(ORDERED_CHILDREN_QUERY,
534                session.getDocument(parentDocumentRef).getId()));
535        DocumentModel sourceDoc;
536        try {
537            sourceDoc = children.get(idx);
538            addRouteElementToRoute(parentDocumentRef, sourceDoc.getName(), routeElement, session);
539        } catch (IndexOutOfBoundsException e) {
540            addRouteElementToRoute(parentDocumentRef, null, routeElement, session);
541        }
542    }
543
544    // @deprecated since 5.9.2 - Use only routes of type 'graph'
545    @Override
546    @Deprecated
547    public void addRouteElementToRoute(DocumentRef parentDocumentRef, String sourceName,
548            DocumentRouteElement routeElement, CoreSession session) throws DocumentRouteNotLockedException {
549        DocumentRoute parentRoute = getParentRouteModel(parentDocumentRef, session);
550        if (!isLockedByCurrentUser(parentRoute, session)) {
551            throw new DocumentRouteNotLockedException();
552        }
553        PathSegmentService pss = Framework.getService(PathSegmentService.class);
554        DocumentModel docRouteElement = routeElement.getDocument();
555        DocumentModel parentDocument = session.getDocument(parentDocumentRef);
556        docRouteElement.setPathInfo(parentDocument.getPathAsString(), pss.generatePathSegment(docRouteElement));
557        String lifecycleState = parentDocument.getCurrentLifeCycleState().equals(
558                DocumentRouteElement.ElementLifeCycleState.draft.name()) ? DocumentRouteElement.ElementLifeCycleState.draft.name()
559                : DocumentRouteElement.ElementLifeCycleState.ready.name();
560        docRouteElement.putContextData(LifeCycleConstants.INITIAL_LIFECYCLE_STATE_OPTION_NAME, lifecycleState);
561        docRouteElement = session.createDocument(docRouteElement);
562        session.orderBefore(parentDocumentRef, docRouteElement.getName(), sourceName);
563        session.save();// the new document will be queried later on
564    }
565
566    @Override
567    public void removeRouteElement(DocumentRouteElement routeElement, CoreSession session)
568            throws DocumentRouteNotLockedException {
569        DocumentRoute parentRoute = routeElement.getDocumentRoute(session);
570        if (!isLockedByCurrentUser(parentRoute, session)) {
571            throw new DocumentRouteNotLockedException();
572        }
573        session.removeDocument(routeElement.getDocument().getRef());
574        session.save();// the document will be queried later on
575    }
576
577    @Override
578    public DocumentModelList getOrderedRouteElement(String routeElementId, CoreSession session) {
579        String query = String.format(ORDERED_CHILDREN_QUERY, routeElementId);
580        DocumentModelList orderedChildren = session.query(query);
581        return orderedChildren;
582    }
583
584    @Override
585    public void lockDocumentRoute(DocumentRoute routeModel, CoreSession session)
586            throws DocumentRouteAlredayLockedException {
587        LockableDocumentRoute lockableRoute = routeModel.getDocument().getAdapter(LockableDocumentRoute.class);
588        boolean lockedByCurrent = isLockedByCurrentUser(routeModel, session);
589        if (lockableRoute.isLocked(session) && !lockedByCurrent) {
590            throw new DocumentRouteAlredayLockedException();
591        }
592        if (!lockedByCurrent) {
593            lockableRoute.lockDocument(session);
594        }
595    }
596
597    @Override
598    public void unlockDocumentRoute(DocumentRoute routeModel, CoreSession session)
599            throws DocumentRouteNotLockedException {
600        LockableDocumentRoute lockableRoute = routeModel.getDocument().getAdapter(LockableDocumentRoute.class);
601        if (!lockableRoute.isLockedByCurrentUser(session)) {
602            throw new DocumentRouteNotLockedException();
603        }
604        lockableRoute.unlockDocument(session);
605    }
606
607    @Override
608    public boolean isLockedByCurrentUser(DocumentRoute routeModel, CoreSession session) {
609        LockableDocumentRoute lockableRoute = routeModel.getDocument().getAdapter(LockableDocumentRoute.class);
610        return lockableRoute.isLockedByCurrentUser(session);
611    }
612
613    @Override
614    public void updateRouteElement(DocumentRouteElement routeElement, CoreSession session)
615            throws DocumentRouteNotLockedException {
616        if (!isLockedByCurrentUser(routeElement.getDocumentRoute(session), session)) {
617            throw new DocumentRouteNotLockedException();
618        }
619        routeElement.save(session);
620    }
621
622    private DocumentRoute getParentRouteModel(DocumentRef documentRef, CoreSession session) {
623        DocumentModel parentDoc = session.getDocument(documentRef);
624        if (parentDoc.hasFacet(DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_FACET)) {
625            return parentDoc.getAdapter(DocumentRoute.class);
626        }
627        DocumentRouteElement rElement = parentDoc.getAdapter(DocumentRouteElement.class);
628        return rElement.getDocumentRoute(session);
629
630    }
631
632    @Override
633    public DocumentRoute saveRouteAsNewModel(DocumentRoute instance, CoreSession session) {
634        DocumentModel instanceModel = instance.getDocument();
635        DocumentModel parent = persister.getParentFolderForNewModel(session, instanceModel);
636        String newName = persister.getNewModelName(instanceModel);
637        DocumentModel newmodel = persister.saveDocumentRouteInstanceAsNewModel(instanceModel, parent, newName, session);
638        DocumentRoute newRoute = newmodel.getAdapter(DocumentRoute.class);
639        if (!newRoute.isDraft()) {
640            newRoute.followTransition(DocumentRouteElement.ElementLifeCycleTransistion.toDraft, session, false);
641        }
642        newRoute.getDocument().setPropertyValue("dc:title", newName);
643        newRoute.setAttachedDocuments(new ArrayList<>());
644        newRoute.save(session);
645        return newRoute;
646    }
647
648    @Override
649    public boolean isRoutable(DocumentModel doc) {
650        if (doc == null) {
651            return false;
652        }
653        String type = doc.getType();
654        // TODO make configurable
655        return type.equals("File") || type.equals("Note");
656    }
657
658    @Override
659    public void importAllRouteModels(CoreSession session) {
660        for (URL url : getRouteModelTemplateResources()) {
661            importRouteModel(url, true, session);
662        }
663    }
664
665    @Override
666    public DocumentRoute importRouteModel(URL modelToImport, boolean overwrite, CoreSession session) {
667        if (modelToImport == null) {
668            throw new NuxeoException(("No resource containing route templates found"));
669        }
670        Blob blob = new URLBlob(modelToImport);
671        final String file = modelToImport.getFile();
672        DocumentModel doc;
673        try {
674            doc = getFileManager().createDocumentFromBlob(session, blob,
675                    persister.getParentFolderForDocumentRouteModels(session).getPathAsString(), true, file);
676        } catch (IOException e) {
677            throw new NuxeoException(e);
678        }
679        if (doc == null) {
680            throw new NuxeoException("Can not import document " + file);
681        }
682        // remove model from cache if any model with the same id existed
683        if (modelsChache != null) {
684            modelsChache.invalidate(doc.getName());
685        }
686
687        return doc.getAdapter(DocumentRoute.class);
688    }
689
690    protected FileManager getFileManager() {
691        return Framework.getService(FileManager.class);
692    }
693
694    @Override
695    public void activate(ComponentContext context) {
696        super.activate(context);
697        modelsChache = CacheBuilder.newBuilder().maximumSize(100).expireAfterWrite(10, TimeUnit.MINUTES).build();
698        repositoryInitializationHandler = new RouteModelsInitializator();
699        repositoryInitializationHandler.install();
700    }
701
702    @Override
703    public void deactivate(ComponentContext context) {
704        super.deactivate(context);
705        if (repositoryInitializationHandler != null) {
706            repositoryInitializationHandler.uninstall();
707        }
708    }
709
710    @Override
711    public List<URL> getRouteModelTemplateResources() {
712        List<URL> urls = new ArrayList<>();
713        for (URL url : routeResourcesRegistry.getRouteModelTemplateResources()) {
714            urls.add(url); // test contrib parsing and deployment
715        }
716        return urls;
717    }
718
719    @SuppressWarnings("unchecked")
720    @Override
721    public List<DocumentModel> searchRouteModels(CoreSession session, String searchString) {
722        List<DocumentModel> allRouteModels = new ArrayList<>();
723        PageProviderService pageProviderService = Framework.getLocalService(PageProviderService.class);
724        Map<String, Serializable> props = new HashMap<>();
725        props.put(MAX_RESULTS_PROPERTY, PAGE_SIZE_RESULTS_KEY);
726        props.put(CORE_SESSION_PROPERTY, (Serializable) session);
727        PageProvider<DocumentModel> pageProvider;
728        if (StringUtils.isEmpty(searchString)) {
729            pageProvider = (PageProvider<DocumentModel>) pageProviderService.getPageProvider(
730                    DOC_ROUTING_SEARCH_ALL_ROUTE_MODELS_PROVIDER_NAME, null, null, 0L, props);
731        } else {
732            pageProvider = (PageProvider<DocumentModel>) pageProviderService.getPageProvider(
733                    DOC_ROUTING_SEARCH_ROUTE_MODELS_WITH_TITLE_PROVIDER_NAME, null, null, 0L, props, searchString + '%');
734        }
735        allRouteModels.addAll(pageProvider.getCurrentPage());
736        while (pageProvider.isNextPageAvailable()) {
737            pageProvider.nextPage();
738            allRouteModels.addAll(pageProvider.getCurrentPage());
739        }
740        return allRouteModels;
741    }
742
743    @Override
744    public void registerRouteResource(RouteModelResourceType res, RuntimeContext context) {
745        if (res.getPath() != null && res.getId() != null) {
746            if (routeResourcesRegistry.getResource(res.getId()) != null) {
747                routeResourcesRegistry.removeContribution(res);
748            }
749            if (res.getUrl() == null) {
750                res.setUrl(getUrlFromPath(res, context));
751            }
752            routeResourcesRegistry.addContribution(res);
753        }
754    }
755
756    protected URL getUrlFromPath(RouteModelResourceType res, RuntimeContext extensionContext) {
757        String path = res.getPath();
758        if (path == null) {
759            return null;
760        }
761        URL url;
762        try {
763            url = new URL(path);
764        } catch (MalformedURLException e) {
765            url = extensionContext.getLocalResource(path);
766            if (url == null) {
767                url = extensionContext.getResource(path);
768            }
769            if (url == null) {
770                url = res.getClass().getResource(path);
771            }
772        }
773        return url;
774    }
775
776    @Override
777    public DocumentRoute getRouteModelWithId(CoreSession session, String id) {
778        String routeDocModelId = getRouteModelDocIdWithId(session, id);
779        DocumentModel routeDoc = session.getDocument(new IdRef(routeDocModelId));
780        return routeDoc.getAdapter(DocumentRoute.class);
781    }
782
783    @Override
784    public String getRouteModelDocIdWithId(CoreSession session, String id) {
785        if (modelsChache != null) {
786            String routeDocId = modelsChache.getIfPresent(id);
787            if (routeDocId != null) {
788                return routeDocId;
789            }
790        }
791        String query = String.format(ROUTE_MODEL_DOC_ID_WITH_ID_QUERY, NXQL.escapeString(id));
792        List<String> routeIds = new ArrayList<>();
793        try (IterableQueryResult results = session.queryAndFetch(query, "NXQL")) {
794            if (results.size() == 0) {
795                throw new NuxeoException("No route found for id: " + id);
796            }
797            if (results.size() != 1) {
798                throw new NuxeoException("More than one route model found with id: " + id);
799            }
800            for (Map<String, Serializable> map : results) {
801                routeIds.add(map.get("ecm:uuid").toString());
802            }
803        }
804        String routeDocId = routeIds.get(0);
805        if (modelsChache == null) {
806            modelsChache = CacheBuilder.newBuilder().maximumSize(100).expireAfterWrite(10, TimeUnit.MINUTES).build();
807        }
808        modelsChache.put(id, routeDocId);
809        return routeDocId;
810    }
811
812    @Override
813    @Deprecated
814    public void makeRoutingTasks(CoreSession coreSession, final List<Task> tasks) {
815        new UnrestrictedSessionRunner(coreSession) {
816            @Override
817            public void run() {
818                for (Task task : tasks) {
819                    DocumentModel taskDoc = task.getDocument();
820                    taskDoc.addFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME);
821                    session.saveDocument(taskDoc);
822                }
823            }
824        }.runUnrestricted();
825    }
826
827    @Override
828    public void endTask(CoreSession session, Task task, Map<String, Object> data, String status) {
829        String comment = (String) data.get(GraphNode.NODE_VARIABLE_COMMENT);
830        TaskService taskService = Framework.getLocalService(TaskService.class);
831        taskService.endTask(session, (NuxeoPrincipal) session.getPrincipal(), task, comment, null, false);
832
833        Map<String, String> taskVariables = task.getVariables();
834        String routeInstanceId = taskVariables.get(DocumentRoutingConstants.TASK_ROUTE_INSTANCE_DOCUMENT_ID_KEY);
835        if (StringUtils.isEmpty(routeInstanceId)) {
836            throw new DocumentRouteException("Can not resume workflow, no related route");
837        }
838        completeTask(routeInstanceId, null, task, data, status, session);
839        final Map<String, Serializable> extraEventProperties = new HashMap<>();
840        extraEventProperties.put(DocumentRoutingConstants.WORKFLOW_TASK_COMPLETION_ACTION_KEY, status);
841        TaskEventNotificationHelper.notifyTaskEnded(session, (NuxeoPrincipal) session.getPrincipal(), task, comment,
842                TaskEventNames.WORKFLOW_TASK_COMPLETED, extraEventProperties);
843
844    }
845
846    @Override
847    public List<DocumentModel> getWorkflowInputDocuments(CoreSession session, Task task) {
848        String routeInstanceId;
849        try {
850            routeInstanceId = task.getProcessId();
851        } catch (PropertyException e) {
852            throw new DocumentRouteException("Can not get the related workflow instance");
853        }
854        if (StringUtils.isEmpty(routeInstanceId)) {
855            throw new DocumentRouteException("Can not get the related workflow instance");
856        }
857        DocumentModel routeDoc;
858        try {
859            routeDoc = session.getDocument(new IdRef(routeInstanceId));
860        } catch (DocumentNotFoundException e) {
861            throw new DocumentRouteException("No workflow with the id:" + routeInstanceId);
862        }
863        DocumentRoute route = routeDoc.getAdapter(DocumentRoute.class);
864        return route.getAttachedDocuments(session);
865    }
866
867    @Override
868    public void grantPermissionToTaskAssignees(CoreSession session, String permission, List<DocumentModel> docs,
869            Task task) {
870        setAclForActors(session, getRoutingACLName(task), permission, docs, task.getActors());
871    }
872
873    @Override
874    public void grantPermissionToTaskDelegatedActors(CoreSession session, String permission, List<DocumentModel> docs,
875            Task task) {
876        setAclForActors(session, getDelegationACLName(task), permission, docs, task.getDelegatedActors());
877    }
878
879    @Override
880    public void removePermissionFromTaskAssignees(CoreSession session, final List<DocumentModel> docs, Task task) {
881        final String aclName = getRoutingACLName(task);
882        new UnrestrictedSessionRunner(session) {
883            @Override
884            public void run() {
885                for (DocumentModel doc : docs) {
886                    ACP acp = doc.getACP();
887                    acp.removeACL(aclName);
888                    doc.setACP(acp, true);
889                    session.saveDocument(doc);
890                }
891            }
892        }.runUnrestricted();
893    }
894
895    /**
896     * @since 7.4
897     */
898    @Override
899    public void removePermissionsForTaskActors(CoreSession session, final List<DocumentModel> docs, String taskId) {
900        final String aclRoutingName = getRoutingACLName(taskId);
901        final String aclDelegationName = getDelegationACLName(taskId);
902        new UnrestrictedSessionRunner(session) {
903            @Override
904            public void run() {
905                for (DocumentModel doc : docs) {
906                    ACP acp = doc.getACP();
907                    acp.removeACL(aclRoutingName);
908                    acp.removeACL(aclDelegationName);
909                    doc.setACP(acp, true);
910                    session.saveDocument(doc);
911                }
912            }
913        }.runUnrestricted();
914    }
915
916    @Override
917    public void removePermissionsForTaskActors(CoreSession session, final List<DocumentModel> docs, Task task) {
918        removePermissionsForTaskActors(session, docs, task.getId());
919    }
920
921    /**
922     * Finds an ACL name specific to the task (there may be several tasks applying permissions to the same document).
923     */
924    protected static String getRoutingACLName(Task task) {
925        return getRoutingACLName(task.getId());
926    }
927
928    /**
929     * @since 7.4
930     */
931    protected static String getRoutingACLName(String taskId) {
932        return DocumentRoutingConstants.DOCUMENT_ROUTING_ACL + '/' + taskId;
933    }
934
935    protected static String getDelegationACLName(Task task) {
936        return getDelegationACLName(task.getId());
937    }
938
939    /**
940     * @since 7.4
941     */
942    protected static String getDelegationACLName(String taskId) {
943        return DocumentRoutingConstants.DOCUMENT_ROUTING_DELEGATION_ACL + '/' + taskId;
944    }
945
946    /**
947     * @since 7.1
948     */
949    private final class WfCleaner extends UnrestrictedSessionRunner {
950        private final int limit;
951
952        protected int i = 0;
953
954        private WfCleaner(String repositoryName, int limit) {
955            super(repositoryName);
956            this.limit = limit;
957        }
958
959        @Override
960        public void run() {
961            List<String> routeIds = new ArrayList<>();
962            String query = "SELECT ecm:uuid FROM DocumentRoute WHERE (ecm:currentLifeCycleState = 'done' "
963                    + "OR ecm:currentLifeCycleState = 'canceled') ORDER BY dc:created";
964            try (IterableQueryResult results = session.queryAndFetch(query, "NXQL")) {
965                for (Map<String, Serializable> result : results) {
966                    routeIds.add(result.get("ecm:uuid").toString());
967                    i++;
968                    // stop when the limit is reached and close the resultSet
969                    if (i == limit) {
970                        break;
971                    }
972                }
973            }
974            for (String routeDocId : routeIds) {
975                final String associatedTaskQuery = String.format(
976                        "SELECT ecm:uuid FROM Document WHERE ecm:mixinType = 'Task' AND nt:processId = '%s'",
977                        routeDocId);
978                try (IterableQueryResult tasks = session.queryAndFetch(associatedTaskQuery, "NXQL")) {
979                    for (Map<String, Serializable> task : tasks) {
980                        final String taskId = task.get("ecm:uuid").toString();
981                        session.removeDocument(new IdRef(taskId));
982                    }
983                }
984                session.removeDocument(new IdRef(routeDocId));
985            }
986        }
987
988        public int getNumberOfCleanedUpWf() {
989            return i;
990        }
991    }
992
993    class UnrestrictedQueryRunner extends UnrestrictedSessionRunner {
994
995        String query;
996
997        DocumentModelList docs;
998
999        protected UnrestrictedQueryRunner(CoreSession session, String query) {
1000            super(session);
1001            this.query = query;
1002        }
1003
1004        @Override
1005        public void run() {
1006            docs = session.query(query);
1007            for (DocumentModel documentModel : docs) {
1008                documentModel.detach(true);
1009            }
1010        }
1011
1012        public DocumentModelList runQuery() {
1013            runUnrestricted();
1014            return docs;
1015        }
1016    }
1017
1018    @Override
1019    public void finishTask(CoreSession session, DocumentRoute route, Task task, boolean delete)
1020            throws DocumentRouteException {
1021        DocumentModelList docs = route.getAttachedDocuments(session);
1022        try {
1023            removePermissionsForTaskActors(session, docs, task);
1024            // delete task
1025            if (delete) {
1026                session.removeDocument(new IdRef(task.getId()));
1027            }
1028        } catch (DocumentNotFoundException e) {
1029            throw new DocumentRouteException("Cannot finish task", e);
1030        }
1031    }
1032
1033    @Override
1034    public void cancelTask(CoreSession session, final String taskId) throws DocumentRouteException {
1035        new UnrestrictedSessionRunner(session) {
1036            @Override
1037            public void run() {
1038                DocumentModel taskDoc = session.getDocument(new IdRef(taskId));
1039                Task task = taskDoc.getAdapter(Task.class);
1040                if (task == null) {
1041                    throw new DocumentRouteException("Invalid taskId: " + taskId);
1042                }
1043
1044                if (!task.isOpened()) {
1045                    log.info("Can not cancel task " + taskId + "as is not open");
1046                    return;
1047                }
1048                task.cancel(session);
1049
1050                // if the task was created by an workflow , update info
1051                String routeId = task.getProcessId();
1052                if (routeId != null) {
1053                    DocumentModel routeDoc = session.getDocument(new IdRef(routeId));
1054                    GraphRoute routeInstance = routeDoc.getAdapter(GraphRoute.class);
1055                    if (routeInstance == null) {
1056                        throw new DocumentRouteException("Invalid routeInstanceId: " + routeId);
1057                    }
1058
1059                    DocumentModelList docs = routeInstance.getAttachedDocumentModels();
1060                    removePermissionsForTaskActors(session, docs, task);
1061                    // task is considered processed with the status "null"
1062                    // when
1063                    // is
1064                    // canceled
1065                    updateTaskInfo(session, routeInstance, task, null);
1066                }
1067                session.saveDocument(task.getDocument());
1068
1069            }
1070        }.runUnrestricted();
1071    }
1072
1073    protected void updateTaskInfo(CoreSession session, GraphRoute graph, Task task, String status) {
1074        String nodeId = task.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY);
1075        if (StringUtils.isEmpty(nodeId)) {
1076            throw new DocumentRouteException("No nodeId found on task: " + task.getId());
1077        }
1078        GraphNode node = graph.getNode(nodeId);
1079
1080        NuxeoPrincipal principal = (NuxeoPrincipal) session.getPrincipal();
1081        String actor = principal.getActingUser();
1082        node.updateTaskInfo(task.getId(), true, status, actor, null);
1083    }
1084
1085    @Override
1086    public void reassignTask(CoreSession session, final String taskId, final List<String> actors, final String comment)
1087            throws DocumentRouteException {
1088        new UnrestrictedSessionRunner(session) {
1089
1090            @Override
1091            public void run() {
1092                DocumentModel taskDoc = session.getDocument(new IdRef(taskId));
1093                Task task = taskDoc.getAdapter(Task.class);
1094                if (task == null) {
1095                    throw new DocumentRouteException("Invalid taskId: " + taskId);
1096                }
1097                if (!task.isOpened()) {
1098                    throw new DocumentRouteException("Task  " + taskId + " is not opened, can not reassign it");
1099                }
1100                String routeId = task.getProcessId();
1101                if (routeId != null) {
1102                    DocumentModel routeDoc = session.getDocument(new IdRef(routeId));
1103                    GraphRoute routeInstance = routeDoc.getAdapter(GraphRoute.class);
1104                    if (routeInstance == null) {
1105                        throw new DocumentRouteException("Invalid routeInstanceId: " + routeId
1106                                + " referenced by the task " + taskId);
1107                    }
1108                    GraphNode node = routeInstance.getNode(task.getType());
1109                    if (node == null) {
1110                        throw new DocumentRouteException("Invalid node " + routeId + " referenced by the task "
1111                                + taskId);
1112                    }
1113                    if (!node.allowTaskReassignment()) {
1114                        throw new DocumentRouteException("Task " + taskId + " can not be reassigned. Node "
1115                                + node.getId() + " doesn't allow reassignment.");
1116                    }
1117                    DocumentModelList docs = routeInstance.getAttachedDocumentModels();
1118                    // remove permissions on the document following the
1119                    // workflow for the current assignees
1120                    removePermissionFromTaskAssignees(session, docs, task);
1121                    Framework.getLocalService(TaskService.class).reassignTask(session, taskId, actors, comment);
1122                    // refresh task
1123                    task.getDocument().refresh();
1124                    // grant permission to the new assignees
1125                    grantPermissionToTaskAssignees(session, node.getTaskAssigneesPermission(), docs, task);
1126
1127                    // Audit task reassignment
1128                    Map<String, Serializable> eventProperties = new HashMap<>();
1129                    eventProperties.put(DocumentEventContext.CATEGORY_PROPERTY_KEY,
1130                            DocumentRoutingConstants.ROUTING_CATEGORY);
1131                    eventProperties.put("taskName", task.getName());
1132                    eventProperties.put("actors", (Serializable) actors);
1133                    eventProperties.put("modelId", routeInstance.getModelId());
1134                    eventProperties.put("modelName", routeInstance.getModelName());
1135                    eventProperties.put(RoutingAuditHelper.WORKFLOW_INITATIOR, routeInstance.getInitiator());
1136                    eventProperties.put(RoutingAuditHelper.TASK_ACTOR,
1137                            ((NuxeoPrincipal) session.getPrincipal()).getActingUser());
1138                    eventProperties.put("comment", comment);
1139                    // compute duration since workflow started
1140                    long timeSinceWfStarted = RoutingAuditHelper.computeDurationSinceWfStarted(task.getProcessId());
1141                    if (timeSinceWfStarted >= 0) {
1142                        eventProperties.put(RoutingAuditHelper.TIME_SINCE_WF_STARTED, timeSinceWfStarted);
1143                    }
1144                    // compute duration since task started
1145                    long timeSinceTaskStarted = RoutingAuditHelper.computeDurationSinceTaskStarted(task.getId());
1146                    if (timeSinceWfStarted >= 0) {
1147                        eventProperties.put(RoutingAuditHelper.TIME_SINCE_TASK_STARTED, timeSinceTaskStarted);
1148                    }
1149                    DocumentEventContext envContext = new DocumentEventContext(session, session.getPrincipal(),
1150                            task.getDocument());
1151                    envContext.setProperties(eventProperties);
1152                    EventProducer eventProducer = Framework.getLocalService(EventProducer.class);
1153                    eventProducer.fireEvent(envContext.newEvent(DocumentRoutingConstants.Events.afterWorkflowTaskReassigned.name()));
1154                }
1155            }
1156        }.runUnrestricted();
1157    }
1158
1159    @Override
1160    public void delegateTask(CoreSession session, final String taskId, final List<String> delegatedActors,
1161            final String comment) throws DocumentRouteException {
1162        new UnrestrictedSessionRunner(session) {
1163
1164            @Override
1165            public void run() {
1166                DocumentModel taskDoc = session.getDocument(new IdRef(taskId));
1167                Task task = taskDoc.getAdapter(Task.class);
1168                if (task == null) {
1169                    throw new DocumentRouteException("Invalid taskId: " + taskId);
1170                }
1171                String routeId = task.getProcessId();
1172                if (routeId != null) {
1173                    DocumentModel routeDoc = session.getDocument(new IdRef(routeId));
1174                    GraphRoute routeInstance = routeDoc.getAdapter(GraphRoute.class);
1175                    if (routeInstance == null) {
1176                        throw new DocumentRouteException("Invalid routeInstanceId: " + routeId
1177                                + " referenced by the task " + taskId);
1178                    }
1179                    GraphNode node = routeInstance.getNode(task.getType());
1180                    if (node == null) {
1181                        throw new DocumentRouteException("Invalid node " + routeId + " referenced by the task "
1182                                + taskId);
1183                    }
1184                    DocumentModelList docs = routeInstance.getAttachedDocumentModels();
1185                    Framework.getLocalService(TaskService.class)
1186                             .delegateTask(session, taskId, delegatedActors, comment);
1187                    // refresh task
1188                    task.getDocument().refresh();
1189                    // grant permission to the new assignees
1190                    grantPermissionToTaskDelegatedActors(session, node.getTaskAssigneesPermission(), docs, task);
1191
1192                    // Audit task delegation
1193                    Map<String, Serializable> eventProperties = new HashMap<>();
1194                    eventProperties.put(DocumentEventContext.CATEGORY_PROPERTY_KEY,
1195                            DocumentRoutingConstants.ROUTING_CATEGORY);
1196                    eventProperties.put("taskName", task.getName());
1197                    eventProperties.put("delegatedActors", (Serializable) delegatedActors);
1198                    eventProperties.put("modelId", routeInstance.getModelId());
1199                    eventProperties.put("modelName", routeInstance.getModelName());
1200                    eventProperties.put(RoutingAuditHelper.WORKFLOW_INITATIOR, routeInstance.getInitiator());
1201                    eventProperties.put(RoutingAuditHelper.TASK_ACTOR,
1202                            ((NuxeoPrincipal) session.getPrincipal()).getActingUser());
1203                    eventProperties.put("comment", comment);
1204
1205                    // compute duration since workflow started
1206                    long timeSinceWfStarted = RoutingAuditHelper.computeDurationSinceWfStarted(task.getProcessId());
1207                    if (timeSinceWfStarted >= 0) {
1208                        eventProperties.put(RoutingAuditHelper.TIME_SINCE_WF_STARTED, timeSinceWfStarted);
1209                    }
1210                    // compute duration since task started
1211                    long timeSinceTaskStarted = RoutingAuditHelper.computeDurationSinceTaskStarted(task.getId());
1212                    if (timeSinceWfStarted >= 0) {
1213                        eventProperties.put(RoutingAuditHelper.TIME_SINCE_TASK_STARTED, timeSinceTaskStarted);
1214                    }
1215
1216                    DocumentEventContext envContext = new DocumentEventContext(session, session.getPrincipal(),
1217                            task.getDocument());
1218                    envContext.setProperties(eventProperties);
1219                    EventProducer eventProducer = Framework.getLocalService(EventProducer.class);
1220                    eventProducer.fireEvent(envContext.newEvent(DocumentRoutingConstants.Events.afterWorkflowTaskDelegated.name()));
1221                }
1222            }
1223        }.runUnrestricted();
1224    }
1225
1226    protected void setAclForActors(CoreSession session, final String aclName, final String permission,
1227            final List<DocumentModel> docs, List<String> actors) {
1228        final List<String> actorIds = new ArrayList<>();
1229        for (String actor : actors) {
1230            if (actor.startsWith(NuxeoPrincipal.PREFIX)) {
1231                actorIds.add(actor.substring(NuxeoPrincipal.PREFIX.length()));
1232            } else if (actor.startsWith(NuxeoGroup.PREFIX)) {
1233                actorIds.add(actor.substring(NuxeoGroup.PREFIX.length()));
1234            } else {
1235                actorIds.add(actor);
1236            }
1237        }
1238        new UnrestrictedSessionRunner(session) {
1239            @Override
1240            public void run() {
1241                for (DocumentModel doc : docs) {
1242                    ACP acp = doc.getACP();
1243                    acp.removeACL(aclName);
1244                    ACL acl = new ACLImpl(aclName);
1245                    for (String actorId : actorIds) {
1246                        acl.add(ACE.builder(actorId, permission).creator(ACTOR_ACE_CREATOR).build());
1247                    }
1248                    acp.addACL(0, acl); // add first to get before blocks
1249                    doc.setACP(acp, true);
1250                    session.saveDocument(doc);
1251                }
1252            }
1253
1254        }.runUnrestricted();
1255    }
1256
1257    @Override
1258    public void cleanupDoneAndCanceledRouteInstances(final String reprositoryName, final int limit) {
1259        doCleanupDoneAndCanceledRouteInstances(reprositoryName, limit);
1260    }
1261
1262    @Override
1263    public int doCleanupDoneAndCanceledRouteInstances(final String reprositoryName, final int limit) {
1264        WfCleaner unrestrictedSessionRunner = new WfCleaner(reprositoryName, limit);
1265        unrestrictedSessionRunner.runUnrestricted();
1266        return unrestrictedSessionRunner.getNumberOfCleanedUpWf();
1267    }
1268
1269    @Override
1270    public void invalidateRouteModelsCache() {
1271        modelsChache.invalidateAll();
1272    }
1273
1274    /**
1275     * @since 7.2
1276     */
1277    @Override
1278    public List<Task> getTasks(final DocumentModel document, String actorId, String workflowInstanceId,
1279            final String worflowModelName, CoreSession session) {
1280        StringBuilder query = new StringBuilder(String.format(
1281                "SELECT * FROM Document WHERE ecm:mixinType = '%s' AND ecm:currentLifeCycleState = '%s'",
1282                TaskConstants.TASK_FACET_NAME, TaskConstants.TASK_OPENED_LIFE_CYCLE_STATE));
1283        if (StringUtils.isNotBlank(actorId)) {
1284            query.append(String.format(" AND nt:actors/* = '%s'", actorId));
1285        }
1286        if (StringUtils.isNotBlank(workflowInstanceId)) {
1287            query.append(String.format(" AND nt:processId = '%s'", workflowInstanceId));
1288        }
1289        if (document != null) {
1290            query.append(String.format(" AND nt:targetDocumentId = '%s'", document.getId()));
1291        }
1292        final DocumentModelList documentModelList = session.query(query.toString());
1293        final List<Task> result = new ArrayList<>();
1294
1295        // User does not necessary have READ on the workflow instance
1296        new UnrestrictedSessionRunner(session) {
1297
1298            @Override
1299            public void run() {
1300                for (DocumentModel documentModel : documentModelList) {
1301                    final Task task = documentModel.getAdapter(Task.class);
1302                    if (StringUtils.isNotBlank(worflowModelName)) {
1303
1304                        final String processId = task.getProcessId();
1305                        if (processId != null && session.exists(new IdRef(processId))) {
1306                            final DocumentRoute routeInstance = session.getDocument(new IdRef(processId)).getAdapter(
1307                                    DocumentRoute.class);
1308                            if (routeInstance != null) {
1309                                final String routeInstanceName = routeInstance.getName();
1310                                if (routeInstanceName != null
1311                                        && (routeInstanceName.equals(worflowModelName) || routeInstanceName.matches("^("
1312                                                + worflowModelName + ")\\.\\d+"))) {
1313                                    result.add(task);
1314                                }
1315                            }
1316                        }
1317                    } else {
1318                        result.add(task);
1319                    }
1320                }
1321            }
1322        }.runUnrestricted();
1323
1324        return result;
1325    }
1326
1327    /**
1328     * @since 7.2
1329     */
1330    @Override
1331    public List<DocumentRoute> getDocumentRelatedWorkflows(DocumentModel document, CoreSession session) {
1332        final String query = String.format(
1333                "SELECT * FROM %s WHERE docri:participatingDocuments/* = '%s' AND ecm:currentLifeCycleState = '%s'",
1334                DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE, document.getId(),
1335                DocumentRouteElement.ElementLifeCycleState.running);
1336        DocumentModelList documentModelList = session.query(query);
1337        List<DocumentRoute> result = new ArrayList<>();
1338        for (DocumentModel documentModel : documentModelList) {
1339            result.add(documentModel.getAdapter(GraphRoute.class));
1340        }
1341        return result;
1342    }
1343
1344    /**
1345     * @since 7.2
1346     */
1347    @Override
1348    public List<DocumentRoute> getRunningWorkflowInstancesLaunchedByCurrentUser(CoreSession session) {
1349        return getRunningWorkflowInstancesLaunchedByCurrentUser(session, null);
1350    }
1351
1352    /**
1353     * @since 7.2
1354     */
1355    @Override
1356    public List<DocumentRoute> getRunningWorkflowInstancesLaunchedByCurrentUser(CoreSession session,
1357            String worflowModelName) {
1358        final String query = String.format(
1359                "SELECT * FROM %s WHERE docri:initiator = '%s' AND ecm:currentLifeCycleState = '%s'",
1360                DocumentRoutingConstants.DOCUMENT_ROUTE_DOCUMENT_TYPE, session.getPrincipal().getName(),
1361                DocumentRouteElement.ElementLifeCycleState.running);
1362        DocumentModelList documentModelList = session.query(query);
1363        List<DocumentRoute> result = new ArrayList<>();
1364        for (DocumentModel documentModel : documentModelList) {
1365            final GraphRoute graphRoute = documentModel.getAdapter(GraphRoute.class);
1366            if (StringUtils.isNotBlank(worflowModelName)) {
1367                final String modelId = graphRoute.getModelId();
1368                if (StringUtils.isNotBlank(modelId)) {
1369                    DocumentRoute model = session.getDocument(new IdRef(modelId)).getAdapter(DocumentRoute.class);
1370                    if (worflowModelName.equals(model.getName())) {
1371                        result.add(graphRoute);
1372                    }
1373                }
1374            } else {
1375                result.add(graphRoute);
1376            }
1377        }
1378        return result;
1379    }
1380
1381    /**
1382     * Returns true id the document route is a model, false if it is just an instance i.e. a running workflow.
1383     *
1384     * @since 7.2
1385     */
1386    @Override
1387    public boolean isWorkflowModel(final DocumentRoute documentRoute) {
1388        return documentRoute.isValidated();
1389    }
1390}