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