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