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