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