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