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