001/*
002 * (C) Copyright 2012-2018 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.platform.routing.core.impl;
020
021import java.io.Serializable;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Calendar;
025import java.util.Collections;
026import java.util.Date;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.Map.Entry;
031
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.lang3.builder.ToStringBuilder;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.nuxeo.ecm.automation.AutomationService;
037import org.nuxeo.ecm.automation.OperationContext;
038import org.nuxeo.ecm.automation.OperationException;
039import org.nuxeo.ecm.automation.core.Constants;
040import org.nuxeo.ecm.automation.core.scripting.DateWrapper;
041import org.nuxeo.ecm.automation.core.scripting.Expression;
042import org.nuxeo.ecm.automation.core.scripting.Scripting;
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.DocumentRef;
047import org.nuxeo.ecm.core.api.IdRef;
048import org.nuxeo.ecm.core.api.NuxeoException;
049import org.nuxeo.ecm.core.api.NuxeoPrincipal;
050import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
051import org.nuxeo.ecm.core.api.model.Property;
052import org.nuxeo.ecm.core.api.model.impl.ListProperty;
053import org.nuxeo.ecm.core.api.model.impl.MapProperty;
054import org.nuxeo.ecm.core.api.validation.DocumentValidationException;
055import org.nuxeo.ecm.core.api.validation.DocumentValidationReport;
056import org.nuxeo.ecm.core.api.validation.DocumentValidationService;
057import org.nuxeo.ecm.core.schema.SchemaManager;
058import org.nuxeo.ecm.core.schema.types.CompositeType;
059import org.nuxeo.ecm.core.schema.types.Schema;
060import org.nuxeo.ecm.core.schema.utils.DateParser;
061import org.nuxeo.ecm.platform.routing.api.DocumentRoute;
062import org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants;
063import org.nuxeo.ecm.platform.routing.api.DocumentRoutingService;
064import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteException;
065import org.nuxeo.ecm.platform.routing.core.api.TasksInfoWrapper;
066import org.nuxeo.ecm.platform.routing.core.api.scripting.RoutingScriptingExpression;
067import org.nuxeo.ecm.platform.routing.core.api.scripting.RoutingScriptingFunctions;
068import org.nuxeo.ecm.platform.task.Task;
069import org.nuxeo.ecm.platform.task.TaskConstants;
070import org.nuxeo.runtime.api.Framework;
071
072/**
073 * Graph Node implementation as an adapter over a DocumentModel.
074 *
075 * @since 5.6
076 */
077public class GraphNodeImpl extends DocumentRouteElementImpl implements GraphNode {
078
079    private static final long serialVersionUID = 1L;
080
081    private static final Log log = LogFactory.getLog(GraphNodeImpl.class);
082
083    private static final String EXPR_PREFIX = "expr:";
084
085    private static final String TEMPLATE_START = "@{";
086
087    protected final GraphRouteImpl graph;
088
089    protected State localState;
090
091    /** To be used through getter. */
092    protected List<Transition> inputTransitions;
093
094    /** To be used through getter. */
095    protected List<Transition> outputTransitions;
096
097    /** To be used through getter. */
098    protected List<Button> taskButtons;
099
100    protected List<EscalationRule> escalationRules;
101
102    protected List<TaskInfo> tasksInfo;
103
104    public GraphNodeImpl(DocumentModel doc, GraphRouteImpl graph) {
105        super(doc, new GraphRunner());
106        this.graph = graph;
107        inputTransitions = new ArrayList<>(2);
108    }
109
110    /**
111     * @since 5.7.2
112     */
113    public GraphNodeImpl(DocumentModel doc) {
114        super(doc, new GraphRunner());
115        graph = (GraphRouteImpl) getDocumentRoute(doc.getCoreSession());
116        inputTransitions = new ArrayList<>(2);
117    }
118
119    @Override
120    public String toString() {
121        return new ToStringBuilder(this).append(getId()).toString();
122    }
123
124    protected boolean getBoolean(String propertyName) {
125        return Boolean.TRUE.equals(getProperty(propertyName));
126    }
127
128    protected void incrementProp(String prop) {
129        Long count = (Long) getProperty(prop);
130        if (count == null) {
131            count = Long.valueOf(0);
132        }
133        document.setPropertyValue(prop, Long.valueOf(count.longValue() + 1));
134        saveDocument();
135    }
136
137    protected CoreSession getSession() {
138        return document.getCoreSession();
139    }
140
141    protected void saveDocument() {
142        getSession().saveDocument(document);
143    }
144
145    @Override
146    public String getId() {
147        return (String) getProperty(PROP_NODE_ID);
148    }
149
150    @Override
151    public State getState() {
152        if (localState != null) {
153            return localState;
154        }
155        String s = document.getCurrentLifeCycleState();
156        return State.fromString(s);
157    }
158
159    @Override
160    public void setState(State state) {
161        if (state == null) {
162            throw new NullPointerException("null state");
163        }
164        String lc = state.getLifeCycleState();
165        if (lc == null) {
166            localState = state;
167        } else {
168            localState = null;
169            String oldLc = document.getCurrentLifeCycleState();
170            if (lc.equals(oldLc)) {
171                return;
172            }
173            document.followTransition(state.getTransition());
174            saveDocument();
175        }
176    }
177
178    @Override
179    public boolean isStart() {
180        return getBoolean(PROP_START);
181    }
182
183    @Override
184    public boolean isStop() {
185        return getBoolean(PROP_STOP);
186    }
187
188    @Override
189    public void setCanceled() {
190        log.debug("Canceling " + this);
191        incrementProp(PROP_CANCELED);
192    }
193
194    @Override
195    public long getCanceledCount() {
196        Long c = (Long) getProperty(PROP_CANCELED);
197        return c == null ? 0 : c.longValue();
198    }
199
200    @Override
201    public boolean isMerge() {
202        String merge = (String) getProperty(PROP_MERGE);
203        return StringUtils.isNotEmpty(merge);
204    }
205
206    @Override
207    public String getInputChain() {
208        return (String) getProperty(PROP_INPUT_CHAIN);
209    }
210
211    @Override
212    public String getOutputChain() {
213        return (String) getProperty(PROP_OUTPUT_CHAIN);
214    }
215
216    @Override
217    public boolean hasTask() {
218        return getBoolean(PROP_HAS_TASK);
219    }
220
221    @SuppressWarnings("unchecked")
222    @Override
223    public List<String> getTaskAssignees() {
224        return (List<String>) getProperty(PROP_TASK_ASSIGNEES);
225    }
226
227    public String getTaskAssigneesVar() {
228        return (String) getProperty(PROP_TASK_ASSIGNEES_VAR);
229    }
230
231    @Override
232    public Date getTaskDueDate() {
233        Calendar cal = (Calendar) getProperty(PROP_TASK_DUE_DATE);
234        return cal == null ? null : cal.getTime();
235    }
236
237    @Override
238    public String getTaskDirective() {
239        return (String) getProperty(PROP_TASK_DIRECTIVE);
240    }
241
242    @Override
243    public String getTaskAssigneesPermission() {
244        return (String) getProperty(PROP_TASK_ASSIGNEES_PERMISSION);
245    }
246
247    @Override
248    public String getTaskLayout() {
249        return (String) getProperty(PROP_TASK_LAYOUT);
250    }
251
252    @Override
253    public String getTaskNotificationTemplate() {
254        return (String) getProperty(PROP_TASK_NOTIFICATION_TEMPLATE);
255    }
256
257    @Override
258    public String getTaskDueDateExpr() {
259        return (String) getProperty(PROP_TASK_DUE_DATE_EXPR);
260    }
261
262    @Override
263    public void starting() {
264        // Allow input transitions reevaluation (needed for loop case)
265        for (Transition t : inputTransitions) {
266            t.setResult(false);
267            getSession().saveDocument(t.source.getDocument());
268        }
269        // Increment node counter
270        incrementProp(PROP_COUNT);
271        document.setPropertyValue(PROP_NODE_START_DATE, Calendar.getInstance());
272        // reset taskInfo property
273        tasksInfo = null;
274        document.setPropertyValue(PROP_TASKS_INFO, new ArrayList<TaskInfo>());
275        saveDocument();
276    }
277
278    @Override
279    public void ending() {
280        document.setPropertyValue(PROP_NODE_END_DATE, Calendar.getInstance());
281        saveDocument();
282    }
283
284    @Override
285    public Map<String, Serializable> getVariables() {
286        return GraphVariablesUtil.getVariables(document, PROP_VARIABLES_FACET);
287    }
288
289    @Override
290    public Map<String, Serializable> getJsonVariables() {
291        return GraphVariablesUtil.getVariables(document, PROP_VARIABLES_FACET, true);
292    }
293
294    @Override
295    public void setVariables(Map<String, Serializable> map) {
296        GraphVariablesUtil.setVariables(document, PROP_VARIABLES_FACET, map);
297    }
298
299    @Override
300    public void setJSONVariables(Map<String, String> map) {
301        GraphVariablesUtil.setJSONVariables(document, PROP_VARIABLES_FACET, map);
302    }
303
304    @SuppressWarnings("unchecked")
305    @Override
306    public void setAllVariables(Map<String, Object> map) {
307        setAllVariables(map, true);
308    }
309
310    @SuppressWarnings("unchecked")
311    @Override
312    public void setAllVariables(Map<String, Object> map, final boolean allowGlobalVariablesAssignement) {
313        if (map == null) {
314            return;
315        }
316        Boolean mapToJSON = Boolean.FALSE;
317        if (map.containsKey(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON)
318                && (Boolean) map.get(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON)) {
319            mapToJSON = Boolean.TRUE;
320        }
321
322        // get variables from node and graph
323        Map<String, Serializable> graphVariables = mapToJSON ? graph.getJsonVariables() : graph.getVariables();
324        Map<String, Serializable> nodeVariables = mapToJSON ? getJsonVariables() : getVariables();
325        Map<String, Serializable> changedGraphVariables = new HashMap<>();
326        Map<String, Serializable> changedNodeVariables = new HashMap<>();
327
328        // set variables back into node and graph
329        if (map.get(Constants.VAR_WORKFLOW_NODE) != null) {
330            for (Entry<String, Serializable> es : ((Map<String, Serializable>) map.get(
331                    Constants.VAR_WORKFLOW_NODE)).entrySet()) {
332                String key = es.getKey();
333                Serializable value = es.getValue();
334                if (nodeVariables.containsKey(key)) {
335                    Serializable oldValue = nodeVariables.get(key);
336                    if (!equality(value, oldValue)) {
337                        changedNodeVariables.put(key, value);
338                    }
339                }
340            }
341        }
342        final String transientSchemaName = DocumentRoutingConstants.GLOBAL_VAR_SCHEMA_PREFIX + getId();
343        final SchemaManager schemaManager = Framework.getService(SchemaManager.class);
344        if (map.get(Constants.VAR_WORKFLOW) != null) {
345            final Schema transientSchema = schemaManager.getSchema(transientSchemaName);
346            for (Entry<String, Serializable> es : ((Map<String, Serializable>) map.get(
347                    Constants.VAR_WORKFLOW)).entrySet()) {
348                String key = es.getKey();
349                Serializable value = es.getValue();
350                if (graphVariables.containsKey(key)) {
351                    Serializable oldValue = graphVariables.get(key);
352                    if (!equality(value, oldValue)) {
353                        if (!allowGlobalVariablesAssignement
354                                && (transientSchema == null || !transientSchema.hasField(key))) {
355                            throw new DocumentRouteException(String.format(
356                                    "You don't have the permission to set the workflow variable %s", key));
357                        }
358                        changedGraphVariables.put(key, value);
359                    }
360                }
361            }
362        }
363
364        if (!allowGlobalVariablesAssignement) {
365            // Validation
366            final DocumentModel transientDocumentModel = new DocumentModelImpl(getDocument().getType());
367            transientDocumentModel.copyContent(document);
368            final String transientFacetName = "facet-" + transientSchemaName;
369            CompositeType transientFacet = schemaManager.getFacet(transientFacetName);
370            if (transientFacet != null) {
371                changedGraphVariables.put(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON, mapToJSON);
372                transientDocumentModel.addFacet("facet-" + transientSchemaName);
373                GraphVariablesUtil.setVariables(transientDocumentModel, "facet-" + transientSchemaName,
374                        changedGraphVariables, false);
375            }
376            changedNodeVariables.put(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON, mapToJSON);
377            GraphVariablesUtil.setVariables(transientDocumentModel, PROP_VARIABLES_FACET, changedNodeVariables, false);
378            DocumentValidationService documentValidationService = Framework.getService(DocumentValidationService.class);
379            DocumentValidationReport report = documentValidationService.validate(transientDocumentModel);
380            if (report.hasError()) {
381                throw new DocumentValidationException(report);
382            }
383        }
384
385        if (!changedNodeVariables.isEmpty()) {
386            changedNodeVariables.put(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON, mapToJSON);
387            setVariables(changedNodeVariables);
388        }
389        if (!changedGraphVariables.isEmpty()) {
390            changedGraphVariables.put(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON, mapToJSON);
391            graph.setVariables(changedGraphVariables);
392        }
393    }
394
395    public static boolean equality(Object o1, Object o2) {
396        if (o1 == o2) {
397            return true;
398        }
399        if (o1 == null || o2 == null) {
400            return false;
401        }
402        if (o1 instanceof List && o2.getClass().isArray()) {
403            return Arrays.equals(((List<?>) o1).toArray(), (Object[]) o2);
404        } else if (o1.getClass().isArray() && o2 instanceof List) {
405            return Arrays.equals((Object[]) o1, ((List<?>) o2).toArray());
406        } else if (o1.getClass().isArray() && o2.getClass().isArray()) {
407            // Nuxeo doesn't use arrays of primitive types
408            return Arrays.equals((Object[]) o1, (Object[]) o2);
409        } else {
410            return o1.equals(o2);
411        }
412    }
413
414    protected OperationContext getExecutionContext(CoreSession session) {
415        OperationContext context = new OperationContext(session);
416        context.putAll(getWorkflowContextualInfo(session, true));
417        context.setCommit(false); // no session save at end
418        DocumentModelList documents = graph.getAttachedDocuments(session);
419        // associated docs
420        context.setInput(documents);
421        return context;
422    }
423
424    @Override
425    public Map<String, Serializable> getWorkflowContextualInfo(CoreSession session, boolean detached) {
426        Map<String, Serializable> context = new HashMap<>();
427        // workflow context
428        context.put("WorkflowVariables", (Serializable) graph.getVariables());
429        context.put("workflowInitiator", getWorkflowInitiator());
430        context.put("workflowStartTime", getWorkflowStartTime());
431        context.put("workflowParent", getWorkflowParentRouteId());
432        context.put("workflowParentNode", getWorkflowParentNodeId());
433        context.put("workflowInstanceId", graph.getDocument().getId());
434        context.put("taskDueTime", (Calendar) getProperty(PROP_TASK_DUE_DATE));
435
436        DocumentModelList documents = graph.getAttachedDocuments(session);
437        if (detached) {
438            for (DocumentModel documentModel : documents) {
439                documentModel.detach(true);
440            }
441        }
442        context.put("workflowDocuments", documents);
443        context.put("documents", documents);
444        // node context
445        String button = (String) getProperty(PROP_NODE_BUTTON);
446        Map<String, Serializable> nodeVariables = getVariables();
447        nodeVariables.put("button", button);
448        nodeVariables.put("numberOfProcessedTasks", getProcessedTasksInfo().size());
449        nodeVariables.put("numberOfTasks", getTasksInfo().size());
450        nodeVariables.put("tasks", new TasksInfoWrapper(getTasksInfo()));
451        context.put("NodeVariables", (Serializable) nodeVariables);
452        context.put("nodeId", getId());
453        String state = getState().name().toLowerCase();
454        context.put("nodeState", state);
455        context.put("state", state);
456        context.put("nodeStartTime", getNodeStartTime());
457        context.put("nodeEndTime", getNodeEndTime());
458        context.put("nodeLastActor", getNodeLastActor());
459
460        // task context
461        context.put("comment", "");
462        return context;
463    }
464
465    protected String getWorkflowInitiator() {
466        return (String) graph.getDocument().getPropertyValue(DocumentRoutingConstants.INITIATOR);
467    }
468
469    protected Calendar getWorkflowStartTime() {
470        return (Calendar) graph.getDocument().getPropertyValue("dc:created");
471    }
472
473    protected String getWorkflowParentRouteId() {
474        return (String) graph.getDocument().getPropertyValue(GraphRoute.PROP_PARENT_ROUTE);
475    }
476
477    protected String getWorkflowParentNodeId() {
478        return (String) graph.getDocument().getPropertyValue(GraphRoute.PROP_PARENT_NODE);
479    }
480
481    protected Calendar getNodeStartTime() {
482        return (Calendar) getDocument().getPropertyValue(PROP_NODE_START_DATE);
483    }
484
485    protected Calendar getNodeEndTime() {
486        return (Calendar) getDocument().getPropertyValue(PROP_NODE_END_DATE);
487    }
488
489    protected String getNodeLastActor() {
490        return (String) getDocument().getPropertyValue(PROP_NODE_LAST_ACTOR);
491    }
492
493    @Override
494    public void executeChain(String chainId) throws DocumentRouteException {
495        executeChain(chainId, null);
496    }
497
498    @Override
499    public void executeTransitionChain(Transition transition) throws DocumentRouteException {
500        executeChain(transition.chain, transition.id);
501    }
502
503    public void executeChain(String chainId, String transitionId) throws DocumentRouteException {
504        // TODO events
505        if (StringUtils.isEmpty(chainId)) {
506            return;
507        }
508
509        // get base context
510        try (OperationContext context = getExecutionContext(getSession())) {
511            if (transitionId != null) {
512                context.put("transition", transitionId);
513            }
514
515            AutomationService automationService = Framework.getService(AutomationService.class);
516            automationService.run(context, chainId);
517
518            setAllVariables(context);
519        } catch (OperationException e) {
520            throw new DocumentRouteException("Error running chain: " + chainId, e);
521        }
522    }
523
524    @Override
525    public void initAddInputTransition(Transition transition) {
526        inputTransitions.add(transition);
527    }
528
529    protected List<Transition> computeOutputTransitions() {
530        ListProperty props = (ListProperty) document.getProperty(PROP_TRANSITIONS);
531        List<Transition> trans = new ArrayList<>(props.size());
532        for (Property p : props) {
533            trans.add(new Transition(this, p));
534        }
535        return trans;
536    }
537
538    @Override
539    public List<Transition> getOutputTransitions() {
540        if (outputTransitions == null) {
541            outputTransitions = computeOutputTransitions();
542        }
543        return outputTransitions;
544    }
545
546    @Override
547    public List<Transition> evaluateTransitions() throws DocumentRouteException {
548        List<Transition> trueTrans = new ArrayList<>();
549        for (Transition t : getOutputTransitions()) {
550            try (OperationContext context = getExecutionContext(getSession())) {
551                context.put("transition", t.id);
552                Expression expr = new RoutingScriptingExpression(t.condition, new RoutingScriptingFunctions(context));
553                Object res = expr.eval(context);
554                // stupid eval() method throws generic Exception
555                if (!(res instanceof Boolean)) {
556                    throw new DocumentRouteException("Condition for transition " + t + " of node '" + getId()
557                            + "' of graph '" + graph.getName() + "' does not evaluate to a boolean: " + t.condition);
558                }
559                boolean bool = Boolean.TRUE.equals(res);
560                t.setResult(bool);
561                if (bool) {
562                    trueTrans.add(t);
563                    if (executeOnlyFirstTransition()) {
564                        // if node is exclusive, no need to evaluate others
565                        break;
566                    }
567                }
568                saveDocument();
569            } catch (DocumentRouteException e) {
570                throw e;
571            } catch (RuntimeException e) {
572                throw new DocumentRouteException("Error evaluating condition: " + t.condition, e);
573            }
574        }
575        return trueTrans;
576    }
577
578    @Override
579    public List<String> evaluateTaskAssignees() throws DocumentRouteException {
580        List<String> taskAssignees = new ArrayList<>();
581        String taskAssigneesVar = getTaskAssigneesVar();
582        if (StringUtils.isEmpty(taskAssigneesVar)) {
583            return taskAssignees;
584        }
585        try (OperationContext context = getExecutionContext(getSession())) {
586            Expression expr = Scripting.newExpression(taskAssigneesVar);
587            Object res = expr.eval(context);
588
589            if (res instanceof List<?>) {
590                res = ((List<?>) res).toArray();
591            }
592            if (res instanceof Object[]) {
593                // try to convert to String[]
594                Object[] list = (Object[]) res;
595                String[] tmp = new String[list.length];
596                try {
597                    System.arraycopy(list, 0, tmp, 0, list.length);
598                    res = tmp;
599                } catch (ArrayStoreException e) {
600                    // one of the elements is not a String
601                }
602            }
603            if (!(res instanceof String || res instanceof String[])) {
604                throw new DocumentRouteException("Can not evaluate task assignees from " + taskAssigneesVar);
605            }
606            if (res instanceof String) {
607                taskAssignees.add((String) res);
608            } else {
609                taskAssignees.addAll(Arrays.asList((String[]) res));
610            }
611        } catch (DocumentRouteException e) {
612            throw e;
613        } catch (RuntimeException e) {
614            throw new DocumentRouteException("Error evaluating task assignees: " + taskAssigneesVar, e);
615        }
616        return taskAssignees;
617    }
618
619    @Override
620    public boolean canMerge() {
621        int n = 0;
622        List<Transition> inputTransitions = getInputTransitions();
623
624        for (Transition t : inputTransitions) {
625            if (t.result) {
626                n++;
627            }
628        }
629        String merge = (String) getProperty(PROP_MERGE);
630        if (MERGE_ONE.equals(merge)) {
631            return n > 0;
632        } else if (MERGE_ALL.equals(merge)) {
633            return n == inputTransitions.size();
634        } else {
635            throw new NuxeoException("Illegal merge mode '" + merge + "' for node " + this);
636        }
637    }
638
639    @Override
640    public List<Transition> getInputTransitions() {
641        return inputTransitions;
642    }
643
644    @Override
645    public void cancelTasks() {
646        CoreSession session = getSession();
647        List<TaskInfo> tasks = getTasksInfo();
648        for (TaskInfo task : tasks) {
649            if (!task.isEnded()) {
650                cancelTask(session, task.getTaskDocId());
651            }
652        }
653    }
654
655    @Override
656    public List<Button> getTaskButtons() {
657        if (taskButtons == null) {
658            taskButtons = computeTaskButtons();
659        }
660        return taskButtons;
661    }
662
663    protected List<Button> computeTaskButtons() {
664        ListProperty props = (ListProperty) document.getProperty(PROP_TASK_BUTTONS);
665        List<Button> btns = new ArrayList<>(props.size());
666        for (Property p : props) {
667            btns.add(new Button(this, p));
668        }
669        Collections.sort(btns);
670        return btns;
671    }
672
673    @Override
674    public void setButton(String status) {
675        document.setPropertyValue(PROP_NODE_BUTTON, status);
676        saveDocument();
677    }
678
679    @Override
680    public void setLastActor(String actor) {
681        document.setPropertyValue(PROP_NODE_LAST_ACTOR, actor);
682        saveDocument();
683    }
684
685    protected void addTaskAssignees(List<String> taskAssignees) {
686        List<String> allTasksAssignees = getTaskAssignees();
687        allTasksAssignees.addAll(taskAssignees);
688        document.setPropertyValue(PROP_TASK_ASSIGNEES, (Serializable) allTasksAssignees);
689        saveDocument();
690    }
691
692    @Override
693    public String getTaskDocType() {
694        String taskDocType = (String) getProperty(PROP_TASK_DOC_TYPE);
695        if (StringUtils.isEmpty(taskDocType) || TaskConstants.TASK_TYPE_NAME.equals(taskDocType)) {
696            taskDocType = DocumentRoutingConstants.ROUTING_TASK_DOC_TYPE;
697        }
698        return taskDocType;
699    }
700
701    protected Date evaluateDueDate() throws DocumentRouteException {
702        String taskDueDateExpr = getTaskDueDateExpr();
703        if (StringUtils.isEmpty(taskDueDateExpr)) {
704            return new Date();
705        }
706        try (OperationContext context = getExecutionContext(getSession())) {
707            Expression expr = Scripting.newExpression(taskDueDateExpr);
708            Object res = expr.eval(context);
709            if (res instanceof DateWrapper) {
710                return ((DateWrapper) res).getDate();
711            } else if (res instanceof Date) {
712                return (Date) res;
713            } else if (res instanceof Calendar) {
714                return ((Calendar) res).getTime();
715            } else if (res instanceof String) {
716                return DateParser.parseW3CDateTime((String) res);
717            } else {
718                throw new DocumentRouteException(
719                        "The following expression can not be evaluated to a date: " + taskDueDateExpr);
720            }
721        } catch (DocumentRouteException e) {
722            throw e;
723        } catch (RuntimeException e) {
724            throw new DocumentRouteException("Error evaluating task due date: " + taskDueDateExpr, e);
725        }
726    }
727
728    @Override
729    public Date computeTaskDueDate() throws DocumentRouteException {
730        Date dueDate = evaluateDueDate();
731        document.setPropertyValue(PROP_TASK_DUE_DATE, dueDate);
732        CoreSession session = document.getCoreSession();
733        session.saveDocument(document);
734        return dueDate;
735    }
736
737    @Override
738    public boolean executeOnlyFirstTransition() {
739        return getBoolean(PROP_EXECUTE_ONLY_FIRST_TRANSITION);
740    }
741
742    @Override
743    public boolean hasSubRoute() throws DocumentRouteException {
744        return getSubRouteModelId() != null;
745    }
746
747    @Override
748    public String getSubRouteModelId() throws DocumentRouteException {
749        String subRouteModelExpr = (String) getProperty(PROP_SUB_ROUTE_MODEL_EXPR);
750        if (StringUtils.isBlank(subRouteModelExpr)) {
751            return null;
752        }
753        try (OperationContext context = getExecutionContext(getSession())) {
754            String res = valueOrExpression(String.class, subRouteModelExpr, context, "Sub-workflow id expression");
755            return StringUtils.defaultIfBlank(res, null);
756        }
757    }
758
759    protected String getSubRouteInstanceId() {
760        return (String) getProperty(GraphNode.PROP_SUB_ROUTE_INSTANCE_ID);
761    }
762
763    @Override
764    public DocumentRoute startSubRoute() throws DocumentRouteException {
765        String subRouteModelId = getSubRouteModelId();
766        // create the instance without starting it
767        DocumentRoutingService service = Framework.getService(DocumentRoutingService.class);
768        List<String> docs = graph.getAttachedDocuments();
769        String subRouteInstanceId = service.createNewInstance(subRouteModelId, docs, getSession(), false);
770        // set info about parent in subroute
771        DocumentModel subRouteInstance = getSession().getDocument(new IdRef(subRouteInstanceId));
772        subRouteInstance.setPropertyValue(GraphRoute.PROP_PARENT_ROUTE, getDocument().getParentRef().toString());
773        subRouteInstance.setPropertyValue(GraphRoute.PROP_PARENT_NODE, getDocument().getName());
774        subRouteInstance = getSession().saveDocument(subRouteInstance);
775        // set info about subroute in parent
776        document.setPropertyValue(PROP_SUB_ROUTE_INSTANCE_ID, subRouteInstanceId);
777        saveDocument();
778        // start the sub-route
779        Map<String, Serializable> map = getSubRouteInitialVariables();
780        service.startInstance(subRouteInstanceId, docs, map, getSession());
781        // return the sub-route
782        // subRouteInstance.refresh();
783        DocumentRoute subRoute = subRouteInstance.getAdapter(DocumentRoute.class);
784        return subRoute;
785    }
786
787    protected Map<String, Serializable> getSubRouteInitialVariables() {
788        ListProperty props = (ListProperty) document.getProperty(PROP_SUB_ROUTE_VARS);
789        Map<String, Serializable> map = new HashMap<>();
790        try (OperationContext context = getExecutionContext(getSession())) {
791            for (Property p : props) {
792                MapProperty prop = (MapProperty) p;
793                String key = (String) prop.get(PROP_KEYVALUE_KEY).getValue();
794                String v = (String) prop.get(PROP_KEYVALUE_VALUE).getValue();
795                Serializable value = valueOrExpression(Serializable.class, v, context,
796                        "Sub-workflow variable expression");
797                map.put(key, value);
798            }
799        }
800        return map;
801    }
802
803    /*
804     * Code similar to the one in OperationChainContribution.
805     */
806    protected <T> T valueOrExpression(Class<T> klass, String v, OperationContext context, String kind)
807            throws DocumentRouteException {
808        if (!v.startsWith(EXPR_PREFIX)) {
809            return (T) v;
810        }
811        v = v.substring(EXPR_PREFIX.length()).trim();
812        Expression expr;
813        if (v.contains(TEMPLATE_START)) {
814            expr = Scripting.newTemplate(v);
815        } else {
816            expr = Scripting.newExpression(v);
817        }
818        Object res = null;
819        try {
820            res = expr.eval(context);
821            // stupid eval() method throws generic Exception
822        } catch (RuntimeException e) {
823            throw new DocumentRouteException("Error evaluating expression: " + v, e);
824        }
825        if (!(klass.isAssignableFrom(res.getClass()))) {
826            throw new DocumentRouteException(
827                    kind + " of node '" + getId() + "' of graph '" + graph.getName() + "' does not evaluate to "
828                            + klass.getSimpleName() + " but " + res.getClass().getName() + ": " + v);
829        }
830        return (T) res;
831    }
832
833    @Override
834    public void cancelSubRoute() throws DocumentRouteException {
835        String subRouteInstanceId = getSubRouteInstanceId();
836        if (!StringUtils.isEmpty(subRouteInstanceId)) {
837            DocumentModel subRouteDoc = getSession().getDocument(new IdRef(subRouteInstanceId));
838            DocumentRoute subRoute = subRouteDoc.getAdapter(DocumentRoute.class);
839            subRoute.cancel(getSession());
840        }
841    }
842
843    protected List<EscalationRule> computeEscalationRules() {
844        ListProperty props = (ListProperty) document.getProperty(PROP_ESCALATION_RULES);
845        List<EscalationRule> rules = new ArrayList<>(props.size());
846        for (Property p : props) {
847            rules.add(new EscalationRule(this, p));
848        }
849        Collections.sort(rules);
850        return rules;
851    }
852
853    @Override
854    public List<EscalationRule> getEscalationRules() {
855        if (escalationRules == null) {
856            escalationRules = computeEscalationRules();
857        }
858        return escalationRules;
859    }
860
861    @Override
862    public List<EscalationRule> evaluateEscalationRules() {
863        List<EscalationRule> rulesToExecute = new ArrayList<>();
864        // add specific helpers for escalation
865        for (EscalationRule rule : getEscalationRules()) {
866            try (OperationContext context = getExecutionContext(getSession())) {
867                Expression expr = new RoutingScriptingExpression(rule.condition,
868                        new RoutingScriptingFunctions(context, rule));
869                Object res = expr.eval(context);
870                if (!(res instanceof Boolean)) {
871                    throw new DocumentRouteException("Condition for rule " + rule + " of node '" + getId()
872                            + "' of graph '" + graph.getName() + "' does not evaluate to a boolean: " + rule.condition);
873                }
874                boolean bool = Boolean.TRUE.equals(res);
875                if ((!rule.isExecuted() || rule.isMultipleExecution()) && bool) {
876                    rulesToExecute.add(rule);
877                }
878            } catch (DocumentRouteException e) {
879                throw e;
880            } catch (RuntimeException e) {
881                throw new DocumentRouteException("Error evaluating condition: " + rule.condition, e);
882            }
883        }
884        saveDocument();
885        return rulesToExecute;
886    }
887
888    @Override
889    public boolean hasMultipleTasks() {
890        return getBoolean(PROP_HAS_MULTIPLE_TASKS);
891    }
892
893    protected List<TaskInfo> computeTasksInfo() {
894        ListProperty props = (ListProperty) document.getProperty(PROP_TASKS_INFO);
895        List<TaskInfo> tasks = new ArrayList<>(props.size());
896        for (Property p : props) {
897            tasks.add(new TaskInfo(this, p));
898        }
899        return tasks;
900    }
901
902    @Override
903    public List<TaskInfo> getTasksInfo() {
904        if (tasksInfo == null) {
905            tasksInfo = computeTasksInfo();
906        }
907        return tasksInfo;
908    }
909
910    @Override
911    public void addTaskInfo(String taskId) {
912        getTasksInfo().add(new TaskInfo(this, taskId));
913        saveDocument();
914    }
915
916    @Override
917    public void removeTaskInfo(String taskId) {
918        ListProperty props = (ListProperty) document.getProperty(PROP_TASKS_INFO);
919        Property propertytoBeRemoved = null;
920        for (Property p : props) {
921            if (taskId.equals(p.get(PROP_TASK_INFO_TASK_DOC_ID).getValue())) {
922                propertytoBeRemoved = p;
923                break;
924            }
925        }
926        if (propertytoBeRemoved != null) {
927            props.remove(propertytoBeRemoved);
928            saveDocument();
929            tasksInfo = null;
930        }
931    }
932
933    @Override
934    public void updateTaskInfo(String taskId, boolean ended, String status, String actor, String comment) {
935        boolean updated = false;
936        List<TaskInfo> tasksInfo = getTasksInfo();
937        for (TaskInfo taskInfo : tasksInfo) {
938            if (taskId.equals(taskInfo.getTaskDocId())) {
939                taskInfo.setComment(comment);
940                taskInfo.setStatus(status);
941                taskInfo.setActor(actor);
942                taskInfo.setEnded(true);
943                updated = true;
944            }
945        }
946        // handle backward compatibility
947        if (!updated) {
948            // task created before 5.7.3;
949            TaskInfo ti = new TaskInfo(this, taskId);
950            ti.setActor(actor);
951            ti.setStatus(status);
952            ti.setComment(comment);
953            ti.setEnded(true);
954            getTasksInfo().add(ti);
955        }
956        saveDocument();
957    }
958
959    @Override
960    public List<TaskInfo> getEndedTasksInfo() {
961        List<TaskInfo> tasksInfo = getTasksInfo();
962        List<TaskInfo> endedTasks = new ArrayList<>();
963        for (TaskInfo taskInfo : tasksInfo) {
964            if (taskInfo.isEnded()) {
965                endedTasks.add(taskInfo);
966            }
967        }
968        return endedTasks;
969    }
970
971    @Override
972    public boolean hasOpenTasks() {
973        return getTasksInfo().size() != getEndedTasksInfo().size();
974    }
975
976    @Override
977    public List<TaskInfo> getProcessedTasksInfo() {
978        List<TaskInfo> tasksInfo = getTasksInfo();
979        List<TaskInfo> processedTasks = new ArrayList<>();
980        for (TaskInfo taskInfo : tasksInfo) {
981            if (taskInfo.isEnded() && taskInfo.getStatus() != null) {
982                processedTasks.add(taskInfo);
983            }
984        }
985        return processedTasks;
986    }
987
988    @Override
989    public boolean allowTaskReassignment() {
990        return getBoolean(PROP_ALLOW_TASK_REASSIGNMENT);
991
992    }
993
994    protected void cancelTask(CoreSession session, final String taskId) throws DocumentRouteException {
995        DocumentRef taskRef = new IdRef(taskId);
996        if (!session.exists(taskRef)) {
997            log.info(String.format("Task with id %s does not exist anymore", taskId));
998            DocumentModelList docs = graph.getAttachedDocumentModels();
999            Framework.getService(DocumentRoutingService.class).removePermissionsForTaskActors(session, docs, taskId);
1000            NuxeoPrincipal principal = session.getPrincipal();
1001            String actor = principal.getActingUser();
1002            updateTaskInfo(taskId, true, null, actor, null);
1003            return;
1004        }
1005        DocumentModel taskDoc = session.getDocument(new IdRef(taskId));
1006        Task task = taskDoc.getAdapter(Task.class);
1007        if (task == null) {
1008            throw new DocumentRouteException("Invalid taskId: " + taskId);
1009        }
1010        DocumentModelList docs = graph.getAttachedDocumentModels();
1011        Framework.getService(DocumentRoutingService.class).removePermissionsForTaskActors(session, docs, task);
1012        if (task.isOpened()) {
1013            task.cancel(session);
1014        }
1015        session.saveDocument(task.getDocument());
1016        // task is considered processed with the status "null" when is
1017        // canceled
1018        // actor
1019        NuxeoPrincipal principal = session.getPrincipal();
1020        String actor = principal.getActingUser();
1021        updateTaskInfo(taskId, true, null, actor, null);
1022    }
1023
1024    @Override
1025    public void setVariable(String name, String value) {
1026        Map<String, Serializable> nodeVariables = getVariables();
1027        if (nodeVariables.containsKey(name)) {
1028            nodeVariables.put(name, value);
1029            setVariables(nodeVariables);
1030        }
1031    }
1032
1033    /**
1034     * @since 7.2
1035     */
1036    @Override
1037    public boolean hasTaskButton(String name) {
1038        for (Button button : getTaskButtons()) {
1039            if (button.getName().equals(name)) {
1040                return true;
1041            }
1042        }
1043        return false;
1044    }
1045
1046}