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