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