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