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