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