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