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