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 *    Mariana Cedica
018 *
019 * $Id$
020 */
021package org.nuxeo.ecm.platform.routing.web;
022
023import static org.jboss.seam.ScopeType.CONVERSATION;
024
025import java.io.Serializable;
026import java.text.ParseException;
027import java.text.SimpleDateFormat;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Collections;
031import java.util.Date;
032import java.util.HashMap;
033import java.util.HashSet;
034import java.util.LinkedHashMap;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038
039import javax.faces.application.FacesMessage;
040import javax.faces.component.EditableValueHolder;
041import javax.faces.component.UIComponent;
042import javax.faces.context.FacesContext;
043import javax.faces.validator.ValidatorException;
044
045import org.apache.commons.lang3.StringUtils;
046import org.apache.commons.logging.Log;
047import org.apache.commons.logging.LogFactory;
048import org.jboss.seam.annotations.In;
049import org.jboss.seam.annotations.Name;
050import org.jboss.seam.annotations.Observer;
051import org.jboss.seam.annotations.Scope;
052import org.jboss.seam.annotations.intercept.BypassInterceptors;
053import org.jboss.seam.annotations.web.RequestParameter;
054import org.jboss.seam.core.Events;
055import org.jboss.seam.faces.FacesMessages;
056import org.jboss.seam.international.StatusMessage;
057import org.nuxeo.ecm.core.api.CoreSession;
058import org.nuxeo.ecm.core.api.DocumentModel;
059import org.nuxeo.ecm.core.api.DocumentNotFoundException;
060import org.nuxeo.ecm.core.api.IdRef;
061import org.nuxeo.ecm.core.api.NuxeoException;
062import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner;
063import org.nuxeo.ecm.core.api.security.SecurityConstants;
064import org.nuxeo.ecm.platform.actions.Action;
065import org.nuxeo.ecm.platform.actions.ActionContext;
066import org.nuxeo.ecm.platform.actions.ELActionContext;
067import org.nuxeo.ecm.platform.actions.ejb.ActionManager;
068import org.nuxeo.ecm.platform.contentview.seam.ContentViewActions;
069import org.nuxeo.ecm.platform.forms.layout.api.BuiltinModes;
070import org.nuxeo.ecm.platform.forms.layout.api.LayoutDefinition;
071import org.nuxeo.ecm.platform.forms.layout.api.LayoutRowDefinition;
072import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition;
073import org.nuxeo.ecm.platform.forms.layout.api.WidgetReference;
074import org.nuxeo.ecm.platform.forms.layout.service.WebLayoutManager;
075import org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants;
076import org.nuxeo.ecm.platform.routing.api.DocumentRoutingService;
077import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteException;
078import org.nuxeo.ecm.platform.routing.core.impl.GraphNode;
079import org.nuxeo.ecm.platform.routing.core.impl.GraphNode.Button;
080import org.nuxeo.ecm.platform.routing.core.impl.GraphRoute;
081import org.nuxeo.ecm.platform.task.Task;
082import org.nuxeo.ecm.platform.task.TaskEventNames;
083import org.nuxeo.ecm.platform.task.TaskImpl;
084import org.nuxeo.ecm.platform.ui.web.api.NavigationContext;
085import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
086import org.nuxeo.ecm.webapp.action.ActionContextProvider;
087import org.nuxeo.ecm.webapp.documentsLists.DocumentsListsManager;
088import org.nuxeo.ecm.webapp.helpers.EventNames;
089import org.nuxeo.runtime.api.Framework;
090
091/**
092 * Task validators
093 *
094 * @since 5.6
095 */
096@Scope(CONVERSATION)
097@Name("routingTaskActions")
098public class RoutingTaskActionsBean implements Serializable {
099
100    private static final long serialVersionUID = 1L;
101
102    private static final Log log = LogFactory.getLog(RoutingTaskActionsBean.class);
103
104    public static final String SUBJECT_PATTERN = "([a-zA-Z_0-9]*(:)[a-zA-Z_0-9]*)";
105
106    /**
107     * Runtime property name, that makes it possible to cache actions available on a given task, depending on its type.
108     * <p>
109     * This caching is global to all tasks in the platform, and will not work correctly if some tasks are filtering some
110     * actions depending on local variables, for instance.
111     *
112     * @since 5.7
113     */
114    public static final String CACHE_ACTIONS_PER_TASK_TYPE_PROP_NAME = "org.nuxeo.routing.cacheActionsPerTaskType";
115
116    @In(create = true, required = false)
117    protected transient CoreSession documentManager;
118
119    @In(required = true, create = true)
120    protected NavigationContext navigationContext;
121
122    @In(create = true, required = false)
123    protected FacesMessages facesMessages;
124
125    @In(create = true)
126    protected Map<String, String> messages;
127
128    @In(create = true)
129    protected transient DocumentsListsManager documentsListsManager;
130
131    @In(create = true, required = false)
132    protected transient ActionContextProvider actionContextProvider;
133
134    @In(create = true, required = false)
135    protected ContentViewActions contentViewActions;
136
137    @RequestParameter("button")
138    protected String button;
139
140    protected ActionManager actionService;
141
142    protected Map<String, TaskInfo> tasksInfoCache = new HashMap<String, TaskInfo>();
143
144    protected Task currentTask;
145
146    protected List<String> formVariablesToKeep;
147
148    public void validateTaskDueDate(FacesContext context, UIComponent component, Object value) {
149        final String DATE_FORMAT = "dd/MM/yyyy";
150        SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
151
152        String messageString = null;
153        if (value != null) {
154            try {
155                Date dueDate = dateFormat.parse(dateFormat.format((Date) value));
156                Date today = dateFormat.parse(dateFormat.format(new Date()));
157                if (dueDate.before(today)) {
158                    messageString = "label.workflow.error.outdated_duedate";
159                }
160            } catch (ParseException e) {
161                messageString = "label.workflow.error.date_parsing";
162            }
163        }
164
165        if (messageString != null) {
166            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context,
167                    "label.workflow.error.outdated_duedate"), null);
168            ((EditableValueHolder) component).setValid(false);
169            context.addMessage(component.getClientId(context), message);
170        }
171    }
172
173    public void validateSubject(FacesContext context, UIComponent component, Object value) {
174        if (!((value instanceof String) && ((String) value).matches(SUBJECT_PATTERN))) {
175            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context,
176                    "label.document.routing.invalid.subject"), null);
177            context.addMessage(null, message);
178            throw new ValidatorException(message);
179        }
180    }
181
182    public String getTaskLayout(Task task) {
183        return getTaskInfo(task, true).layout;
184    }
185
186    public List<Action> getTaskButtons(Task task) {
187        List<Button> buttons = getTaskInfo(task, true).buttons;
188        List<Action> actions = new ArrayList<Action>();
189
190        DocumentModel workflowInstance = documentManager.getDocument(new IdRef(task.getProcessId()));
191        GraphRoute workflow = workflowInstance.getAdapter(GraphRoute.class);
192        if (workflow == null) {
193            // task was not created by a workflow process , no actions to
194            // display;
195            return actions;
196        }
197        GraphNode node = workflow.getNode(task.getType());
198        for (Button button : buttons) {
199            Action action = new Action(button.getName(), Action.EMPTY_CATEGORIES);
200            action.setLabel(button.getLabel());
201            action.setImmediate(!button.getValidate());
202            boolean displayAction = true;
203            if (StringUtils.isNotEmpty(button.getFilter())) {
204                ActionContext actionContext = actionContextProvider.createActionContext();
205                if (node != null) {
206                    Map<String, Object> workflowContextualInfo = new HashMap<String, Object>();
207                    workflowContextualInfo.putAll(node.getWorkflowContextualInfo(documentManager, true));
208                    actionContext.putAllLocalVariables(workflowContextualInfo);
209                }
210                displayAction = getActionService().checkFilter(button.filter, actionContext);
211            }
212            if (displayAction) {
213                actions.add(action);
214            }
215        }
216        return actions;
217    }
218
219    public String endTask(Task task) {
220        // collect form data
221        Map<String, Object> data = new HashMap<String, Object>();
222        Map<String, Serializable> formVariables = getFormVariables(task);
223        if (getFormVariables(task) != null) {
224            data.put("WorkflowVariables", getFormVariables(task));
225            data.put("NodeVariables", getFormVariables(task));
226            // if there is a comment on the submitted form, pass it to be
227            // logged by audit
228            if (formVariables.containsKey(GraphNode.NODE_VARIABLE_COMMENT)) {
229                data.put(GraphNode.NODE_VARIABLE_COMMENT, formVariables.get(GraphNode.NODE_VARIABLE_COMMENT));
230            }
231        }
232        // add the button name that was clicked
233        try {
234            DocumentRoutingService routing = Framework.getService(DocumentRoutingService.class);
235            routing.endTask(documentManager, task, data, button);
236            facesMessages.add(StatusMessage.Severity.INFO, messages.get("workflow.feedback.info.taskEnded"));
237        } catch (DocumentRouteException e) {
238            log.error(e, e);
239            facesMessages.add(StatusMessage.Severity.ERROR, messages.get("workflow.feedback.error.taskEnded"));
240        }
241        Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED);
242        clear(task.getId());
243        if (navigationContext.getCurrentDocument() != null
244                && documentManager.hasPermission(navigationContext.getCurrentDocument().getRef(),
245                        SecurityConstants.READ)) {
246            return null;
247        }
248        // if the user only had temporary permissions on the current doc given
249        // by the workflow
250        navigationContext.setCurrentDocument(null);
251        return navigationContext.goHome();
252    }
253
254    private void clear(String taskId) {
255        button = null;
256        if (tasksInfoCache.containsKey(taskId)) {
257            tasksInfoCache.remove(taskId);
258        }
259    }
260
261    public Map<String, Serializable> getFormVariables(Task task) {
262        return getTaskInfo(task, true).formVariables;
263    }
264
265    public class TaskInfo {
266        protected HashMap<String, Serializable> formVariables;
267
268        protected String layout;
269
270        protected boolean canBeReassigned;
271
272        protected List<Button> buttons;
273
274        protected List<String> actors;
275
276        protected String comment;
277
278        protected String taskId;
279
280        protected String name;
281
282        protected TaskInfo(String taskId, HashMap<String, Serializable> formVariables, String layout,
283                List<Button> buttons, boolean canBeReassigned, String name) {
284            this.formVariables = formVariables;
285            this.layout = layout;
286            this.buttons = buttons;
287            this.canBeReassigned = canBeReassigned;
288            this.taskId = taskId;
289            this.name = name;
290        }
291
292        public List<String> getActors() {
293            return actors;
294        }
295
296        public void setActors(List<String> actors) {
297            this.actors = actors;
298        }
299
300        public String getComment() {
301            return comment;
302        }
303
304        public void setComment(String comment) {
305            this.comment = comment;
306        }
307
308        public boolean isCanBeReassigned() {
309            return canBeReassigned;
310        }
311
312        public String getTaskId() {
313            return taskId;
314        }
315
316        public String getName() {
317            return name;
318        }
319    }
320
321    // we have to be unrestricted to get this info
322    // because the current user may not be the one that started the
323    // workflow
324    public TaskInfo getTaskInfo(final Task task, final boolean getFormVariables) {
325        if (tasksInfoCache.containsKey(task.getId())) {
326            return tasksInfoCache.get(task.getId());
327        }
328        final String routeDocId = task.getVariable(DocumentRoutingConstants.TASK_ROUTE_INSTANCE_DOCUMENT_ID_KEY);
329        final String nodeId = task.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY);
330        if (routeDocId == null) {
331            throw new NuxeoException("Can not get the source graph for this task");
332        }
333        if (nodeId == null) {
334            throw new NuxeoException("Can not get the source node for this task");
335        }
336        final TaskInfo[] res = new TaskInfo[1];
337        new UnrestrictedSessionRunner(documentManager) {
338            @Override
339            public void run() {
340                DocumentModel doc = session.getDocument(new IdRef(routeDocId));
341                GraphRoute route = doc.getAdapter(GraphRoute.class);
342                GraphNode node = route.getNode(nodeId);
343                HashMap<String, Serializable> map = new HashMap<String, Serializable>();
344                if (getFormVariables) {
345                    map.putAll(node.getVariables());
346                    map.putAll(route.getVariables());
347                }
348                res[0] = new TaskInfo(task.getId(), map, node.getTaskLayout(), node.getTaskButtons(),
349                        node.allowTaskReassignment(), task.getName());
350            }
351        }.runUnrestricted();
352        // don't add tasks in cache when are fetched without the form variables
353        // for
354        // bulk processing
355        if (getFormVariables) {
356            tasksInfoCache.put(task.getId(), res[0]);
357        }
358        return res[0];
359    }
360
361    /**
362     * @since 5.6
363     */
364    public boolean isRoutingTask(Task task) {
365        return task.getDocument().hasFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME);
366    }
367
368    /**
369     * @since 5.6
370     */
371    public List<Action> getTaskActions(Task task) {
372        return new ArrayList<Action>(getTaskActionsMap(task).values());
373    }
374
375    // temp method because Studio also refers to empty layouts
376    protected boolean isLayoutEmpty(String layoutName) {
377        if (layoutName == null || layoutName.isEmpty()) {
378            return true;
379        }
380        // explore the layout and find out if it contains only empty widgets
381        WebLayoutManager lm = Framework.getService(WebLayoutManager.class);
382        LayoutDefinition layout = lm.getLayoutDefinition(layoutName);
383        if (layout == null || layout.isEmpty()) {
384            return true;
385        }
386        return false;
387    }
388
389    /**
390     * Helper to generate a unique action id for all task types
391     *
392     * @since 5.7
393     */
394    protected String getTaskActionId(Task task, String buttonId) {
395        return String.format("%s_%s", task.getType(), buttonId);
396    }
397
398    /**
399     * @since 5.6
400     */
401    public Map<String, Action> getTaskActionsMap(Task task) {
402        Map<String, Action> actions = new LinkedHashMap<String, Action>();
403        TaskInfo taskInfo = getTaskInfo(task, true);
404        String layout = taskInfo.layout;
405        List<Button> buttons = taskInfo.buttons;
406
407        boolean addLayout = !isLayoutEmpty(layout);
408        Map<String, Serializable> props = null;
409        if (addLayout) {
410            props = new HashMap<String, Serializable>();
411            props.put("layout", layout);
412            props.put("formVariables", taskInfo.formVariables);
413        }
414
415        if (buttons != null && !buttons.isEmpty()) {
416            for (Button button : buttons) {
417                String buttonId = button.getName();
418                String id = getTaskActionId(task, buttonId);
419                Action action = new Action(id, Action.EMPTY_CATEGORIES);
420                action.setLabel(button.getLabel());
421                Map<String, Serializable> actionProps = new HashMap<String, Serializable>();
422                actionProps.put("buttonId", buttonId);
423                if (addLayout) {
424                    actionProps.putAll(props);
425                    action.setProperties(actionProps);
426                    action.setType("fancybox");
427                } else {
428                    action.setProperties(actionProps);
429                    action.setType("link");
430                }
431                boolean displayAction = true;
432                if (StringUtils.isNotEmpty(button.getFilter())) {
433                    displayAction = getActionService().checkFilter(button.filter,
434                            actionContextProvider.createActionContext());
435                }
436                if (displayAction) {
437                    actions.put(id, action);
438                }
439            }
440        }
441
442        // If there is a form attached to these tasks, add a generic
443        // process action to open the fancy box.
444        // The form of the first task will be displayed, but all the tasks
445        // concerned by this action share the same form, as they share the
446        // same type.
447        if (addLayout && !actions.isEmpty()) {
448            String id = getTaskActionId(task, "process_task");
449            Action processAction = new Action(id, Action.EMPTY_CATEGORIES);
450
451            formVariablesToKeep = new ArrayList<>();
452            WebLayoutManager layoutService = Framework.getService(WebLayoutManager.class);
453            LayoutDefinition taskLayout = layoutService.getLayoutDefinition(taskInfo.layout);
454            if (taskLayout != null) {
455                for (LayoutRowDefinition row : taskLayout.getRows()) {
456                    for (WidgetReference widgetRef : row.getWidgetReferences()) {
457                        WidgetDefinition widgetDefinition = taskLayout.getWidgetDefinition(widgetRef.getName());
458                        if (widgetDefinition == null) {
459                            continue;
460                        }
461
462                        String mode = widgetDefinition.getMode(BuiltinModes.EDIT);
463                        ActionContext el = new ELActionContext();
464                        el.setCurrentPrincipal(documentManager.getPrincipal());
465                        el.setCurrentDocument(navigationContext.getCurrentDocument());
466                        mode = el.evalExpression(mode, String.class);
467                        if (mode == null || mode.equals(BuiltinModes.EDIT)) {
468                            Arrays.stream(widgetDefinition.getFieldDefinitions()).forEach((field) -> {
469                                // workflow form fields are always like "['$variable']"
470                                // remove both [' and '] to keep only the variable name
471                                String fieldName = field.getFieldName().replaceAll("^\\['|']$", "");
472                                formVariablesToKeep.add(fieldName);
473                            });
474                        }
475                    }
476                }
477            }
478
479            processAction.setProperties(props);
480            processAction.setType("process_task");
481            actions.put(id, processAction);
482        }
483
484        return actions;
485    }
486
487    /**
488     * Returns actions for task document buttons defined in the workflow graph
489     *
490     * @since 5.6
491     */
492    @SuppressWarnings("boxing")
493    public List<Action> getTaskActions(String selectionListName) {
494        Map<String, Action> actions = new LinkedHashMap<String, Action>();
495        Map<String, Map<String, Action>> actionsPerTaskType = new LinkedHashMap<String, Map<String, Action>>();
496        Map<String, Integer> actionsCounter = new HashMap<String, Integer>();
497        List<DocumentModel> docs = documentsListsManager.getWorkingList(selectionListName);
498        boolean cachePerType = Boolean.TRUE.equals(Boolean.valueOf(Framework.getProperty(CACHE_ACTIONS_PER_TASK_TYPE_PROP_NAME)));
499        int taskDocsNum = 0;
500        if (docs != null && !docs.isEmpty()) {
501            for (DocumentModel doc : docs) {
502                if (doc.hasFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME)) {
503                    Task task = new TaskImpl(doc);
504                    String taskType = task.getType();
505                    Map<String, Action> taskActions = Collections.emptyMap();
506                    // if caching per type, fill the per type map, else update
507                    // actions directly
508                    if (cachePerType) {
509                        if (actionsPerTaskType.containsKey(taskType)) {
510                            taskActions = actionsPerTaskType.get(taskType);
511                        } else {
512                            taskActions = getTaskActionsMap(task);
513                            actionsPerTaskType.put(taskType, taskActions);
514                        }
515                    } else {
516                        taskActions = getTaskActionsMap(task);
517                        actions.putAll(taskActions);
518                    }
519                    for (String actionId : taskActions.keySet()) {
520                        Integer count = actionsCounter.get(actionId);
521                        if (count == null) {
522                            actionsCounter.put(actionId, 1);
523                        } else {
524                            actionsCounter.put(actionId, count + 1);
525                        }
526                    }
527                    taskDocsNum++;
528                }
529            }
530        }
531        if (cachePerType) {
532            // initialize actions for cache map
533            for (Map<String, Action> actionsPerType : actionsPerTaskType.values()) {
534                actions.putAll(actionsPerType);
535            }
536        }
537        List<Action> res = new ArrayList<Action>(actions.values());
538        for (Action action : res) {
539            if (!actionsCounter.get(action.getId()).equals(taskDocsNum)) {
540                action.setAvailable(false);
541            }
542        }
543        return res;
544    }
545
546    /**
547     * Ends a task given a selection list name and an action
548     *
549     * @since 5.6
550     */
551    @SuppressWarnings("unchecked")
552    public String endTasks(String selectionListName, Action taskAction) {
553        // collect form data
554        Map<String, Object> data = new HashMap<String, Object>();
555        String buttonId = (String) taskAction.getProperties().get("buttonId");
556        Map<String, Serializable> formVariables = (Map<String, Serializable>) taskAction.getProperties().get(
557                "formVariables");
558
559        if (formVariables != null && !formVariables.isEmpty()) {
560            // if there is a comment on the submitted form, pass it to be
561            // logged by audit
562            if (formVariables.containsKey(GraphNode.NODE_VARIABLE_COMMENT)) {
563                data.put(GraphNode.NODE_VARIABLE_COMMENT, formVariables.get(GraphNode.NODE_VARIABLE_COMMENT));
564            }
565        }
566
567        // get task documents
568        boolean hasErrors = false;
569        DocumentRoutingService routing = Framework.getService(DocumentRoutingService.class);
570        List<DocumentModel> docs = documentsListsManager.getWorkingList(selectionListName);
571        if (docs != null && !docs.isEmpty()) {
572            for (DocumentModel doc : docs) {
573                // For each task, compute its own node and workflow variables
574                Task task = new TaskImpl(doc);
575                Map<String, Serializable> variables = getFormVariables(task);
576                for (String fieldName : formVariablesToKeep) {
577                    variables.put(fieldName, formVariables.get(fieldName));
578                }
579                data.put("WorkflowVariables", variables);
580                data.put("NodeVariables", variables);
581                if (doc.hasFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME)) {
582                    // add the button name that was clicked
583                    try {
584                        routing.endTask(documentManager, new TaskImpl(doc), data, buttonId);
585                    } catch (DocumentRouteException e) {
586                        log.error(e, e);
587                        hasErrors = true;
588                    }
589                }
590            }
591            formVariablesToKeep = null;
592        }
593        if (hasErrors) {
594            facesMessages.add(StatusMessage.Severity.ERROR, messages.get("workflow.feedback.error.tasksEnded"));
595        } else {
596            facesMessages.add(StatusMessage.Severity.INFO, messages.get("workflow.feedback.info.tasksEnded"));
597        }
598        // reset selection list
599        documentsListsManager.resetWorkingList(selectionListName);
600        // raise document change event to trigger refresh of content views
601        // listing task documents.
602        Events.instance().raiseEvent(EventNames.DOCUMENT_CHANGED);
603        Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED);
604        return null;
605    }
606
607    private ActionManager getActionService() {
608        if (actionService == null) {
609            actionService = Framework.getService(ActionManager.class);
610        }
611        return actionService;
612    }
613
614    /***
615     * @since 5.7
616     */
617    @Observer(value = { TaskEventNames.WORKFLOW_TASK_COMPLETED, TaskEventNames.WORKFLOW_TASK_REASSIGNED,
618            TaskEventNames.WORKFLOW_TASK_DELEGATED })
619    @BypassInterceptors
620    public void OnTaskCompleted() {
621        if (contentViewActions != null) {
622            contentViewActions.refreshOnSeamEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED);
623            contentViewActions.resetPageProviderOnSeamEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED);
624        }
625        tasksInfoCache.clear();
626        currentTask = null;
627    }
628
629    /**
630     * @since 5.7.3
631     */
632    public String reassignTask(TaskInfo taskInfo) {
633        try {
634            Framework.getService(DocumentRoutingService.class).reassignTask(documentManager, taskInfo.getTaskId(),
635                    taskInfo.getActors(), taskInfo.getComment());
636            Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_REASSIGNED);
637        } catch (DocumentRouteException e) {
638            log.error(e);
639            facesMessages.add(StatusMessage.Severity.ERROR, messages.get("workflow.feedback.error.taskEnded"));
640        }
641        return null;
642    }
643
644    /**
645     * @since 5.7.3
646     */
647    public String getWorkflowTitle(String instanceId) {
648        String workflowTitle = "";
649
650        try {
651            DocumentModel routeInstance = documentManager.getDocument(new IdRef(instanceId));
652            workflowTitle = routeInstance.getTitle();
653        } catch (DocumentNotFoundException e) {
654            log.error("Can not fetch route instance with id " + instanceId, e);
655        }
656        return workflowTitle;
657    }
658
659    /**
660     * @since 5.8
661     */
662    public String delegateTask(TaskInfo taskInfo) {
663        try {
664            Framework.getService(DocumentRoutingService.class).delegateTask(documentManager, taskInfo.getTaskId(),
665                    taskInfo.getActors(), taskInfo.getComment());
666            Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_DELEGATED);
667        } catch (DocumentRouteException e) {
668            log.error(e);
669            facesMessages.add(StatusMessage.Severity.ERROR, messages.get("workflow.feedback.error.taskEnded"));
670        }
671        return null;
672    }
673
674    /**
675     * @since 5.8
676     */
677    public String navigateToTask(DocumentModel taskDoc) {
678        setCurrentTask(taskDoc.getAdapter(Task.class));
679        return null;
680    }
681
682    /**
683     * @since 5.8
684     */
685    public String navigateToTasksView() {
686        setCurrentTask(null);
687        return null;
688    }
689
690    /**
691     * @since 5.8
692     */
693    public Task getCurrentTask() {
694        return currentTask;
695    }
696
697    /**
698     * @since 5.8
699     */
700    public void setCurrentTask(Task currentTask) {
701        this.currentTask = currentTask;
702    }
703
704    /**
705     * Added to avoid an error when opening a task created @before 5.8 see NXP-14047
706     *
707     * @since 5.9.3, 5.8.0-HF10
708     * @return
709     */
710    @SuppressWarnings("deprecation")
711    public List<String> getCurrentTaskTargetDocumentsIds() {
712        Set<String> uniqueTargetDocIds = new HashSet<String>();
713        List<String> docIds = new ArrayList<String>();
714        if (currentTask == null) {
715            return docIds;
716        }
717        uniqueTargetDocIds.addAll(currentTask.getTargetDocumentsIds());
718        docIds.addAll(uniqueTargetDocIds);
719        return docIds.isEmpty() ? null : docIds;
720    }
721
722    /**
723     * @since 5.8 - Define if action reassign task can be displayed.
724     */
725    public boolean canBeReassign() {
726        if (currentTask == null) {
727            return false;
728        }
729        DocumentModel workflowInstance = documentManager.getDocument(new IdRef(currentTask.getProcessId()));
730        GraphRoute workflow = workflowInstance.getAdapter(GraphRoute.class);
731        if (workflow == null) {
732            return false;
733        }
734        GraphNode node = workflow.getNode(currentTask.getType());
735        return node.allowTaskReassignment() && !currentTask.getDelegatedActors().contains(documentManager.getPrincipal().getName());
736    }
737}