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 navigationContext.setCurrentDocument(null); 245 return navigationContext.goHome(); 246 } 247 248 private void clear(String taskId) { 249 button = null; 250 if (tasksInfoCache.containsKey(taskId)) { 251 tasksInfoCache.remove(taskId); 252 } 253 } 254 255 public Map<String, Serializable> getFormVariables(Task task) { 256 return getTaskInfo(task, true).formVariables; 257 } 258 259 public class TaskInfo { 260 protected HashMap<String, Serializable> formVariables; 261 262 protected String layout; 263 264 protected boolean canBeReassigned; 265 266 protected List<Button> buttons; 267 268 protected List<String> actors; 269 270 protected String comment; 271 272 protected String taskId; 273 274 protected String name; 275 276 protected TaskInfo(String taskId, HashMap<String, Serializable> formVariables, String layout, 277 List<Button> buttons, boolean canBeReassigned, String name) { 278 this.formVariables = formVariables; 279 this.layout = layout; 280 this.buttons = buttons; 281 this.canBeReassigned = canBeReassigned; 282 this.taskId = taskId; 283 this.name = name; 284 } 285 286 public List<String> getActors() { 287 return actors; 288 } 289 290 public void setActors(List<String> actors) { 291 this.actors = actors; 292 } 293 294 public String getComment() { 295 return comment; 296 } 297 298 public void setComment(String comment) { 299 this.comment = comment; 300 } 301 302 public boolean isCanBeReassigned() { 303 return canBeReassigned; 304 } 305 306 public String getTaskId() { 307 return taskId; 308 } 309 310 public String getName() { 311 return name; 312 } 313 } 314 315 // we have to be unrestricted to get this info 316 // because the current user may not be the one that started the 317 // workflow 318 public TaskInfo getTaskInfo(final Task task, final boolean getFormVariables) { 319 if (tasksInfoCache.containsKey(task.getId())) { 320 return tasksInfoCache.get(task.getId()); 321 } 322 final String routeDocId = task.getVariable(DocumentRoutingConstants.TASK_ROUTE_INSTANCE_DOCUMENT_ID_KEY); 323 final String nodeId = task.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY); 324 if (routeDocId == null) { 325 throw new NuxeoException("Can not get the source graph for this task"); 326 } 327 if (nodeId == null) { 328 throw new NuxeoException("Can not get the source node for this task"); 329 } 330 final TaskInfo[] res = new TaskInfo[1]; 331 new UnrestrictedSessionRunner(documentManager) { 332 @Override 333 public void run() { 334 DocumentModel doc = session.getDocument(new IdRef(routeDocId)); 335 GraphRoute route = doc.getAdapter(GraphRoute.class); 336 GraphNode node = route.getNode(nodeId); 337 HashMap<String, Serializable> map = new HashMap<String, Serializable>(); 338 if (getFormVariables) { 339 map.putAll(node.getVariables()); 340 map.putAll(route.getVariables()); 341 } 342 res[0] = new TaskInfo(task.getId(), map, node.getTaskLayout(), node.getTaskButtons(), 343 node.allowTaskReassignment(), task.getName()); 344 } 345 }.runUnrestricted(); 346 // don't add tasks in cache when are fetched without the form variables 347 // for 348 // bulk processing 349 if (getFormVariables) { 350 tasksInfoCache.put(task.getId(), res[0]); 351 } 352 return res[0]; 353 } 354 355 /** 356 * @since 5.6 357 */ 358 public boolean isRoutingTask(Task task) { 359 return task.getDocument().hasFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME); 360 } 361 362 /** 363 * @since 5.6 364 */ 365 public List<Action> getTaskActions(Task task) { 366 return new ArrayList<Action>(getTaskActionsMap(task).values()); 367 } 368 369 // temp method because Studio also refers to empty layouts 370 protected boolean isLayoutEmpty(String layoutName) { 371 if (layoutName == null || layoutName.isEmpty()) { 372 return true; 373 } 374 // explore the layout and find out if it contains only empty widgets 375 WebLayoutManager lm = Framework.getService(WebLayoutManager.class); 376 LayoutDefinition layout = lm.getLayoutDefinition(layoutName); 377 if (layout == null || layout.isEmpty()) { 378 return true; 379 } 380 return false; 381 } 382 383 /** 384 * Helper to generate a unique action id for all task types 385 * 386 * @since 5.7 387 */ 388 protected String getTaskActionId(Task task, String buttonId) { 389 return String.format("%s_%s", task.getType(), buttonId); 390 } 391 392 /** 393 * @since 5.6 394 */ 395 public Map<String, Action> getTaskActionsMap(Task task) { 396 Map<String, Action> actions = new LinkedHashMap<String, Action>(); 397 // bulk processing, don't fetch formVariables to avoid overriding them 398 TaskInfo taskInfo = getTaskInfo(task, false); 399 String layout = taskInfo.layout; 400 List<Button> buttons = taskInfo.buttons; 401 402 boolean addLayout = !isLayoutEmpty(layout); 403 Map<String, Serializable> props = null; 404 if (addLayout) { 405 props = new HashMap<String, Serializable>(); 406 props.put("layout", layout); 407 props.put("formVariables", taskInfo.formVariables); 408 } 409 410 if (buttons != null && !buttons.isEmpty()) { 411 for (Button button : buttons) { 412 String buttonId = button.getName(); 413 String id = getTaskActionId(task, buttonId); 414 Action action = new Action(id, Action.EMPTY_CATEGORIES); 415 action.setLabel(button.getLabel()); 416 Map<String, Serializable> actionProps = new HashMap<String, Serializable>(); 417 actionProps.put("buttonId", buttonId); 418 if (addLayout) { 419 actionProps.putAll(props); 420 action.setProperties(actionProps); 421 action.setType("fancybox"); 422 } else { 423 action.setProperties(actionProps); 424 action.setType("link"); 425 } 426 boolean displayAction = true; 427 if (StringUtils.isNotEmpty(button.getFilter())) { 428 displayAction = getActionService().checkFilter(button.filter, 429 actionContextProvider.createActionContext()); 430 } 431 if (displayAction) { 432 actions.put(id, action); 433 } 434 } 435 } 436 437 // If there is a form attached to these tasks, add a generic 438 // process action to open the fancy box. 439 // The form of the first task will be displayed, but all the tasks 440 // concerned by this action share the same form, as they share the 441 // same type. 442 if (addLayout && !actions.isEmpty()) { 443 String id = getTaskActionId(task, "process_task"); 444 Action processAction = new Action(id, Action.EMPTY_CATEGORIES); 445 processAction.setProperties(props); 446 processAction.setType("process_task"); 447 actions.put(id, processAction); 448 } 449 450 return actions; 451 } 452 453 /** 454 * Returns actions for task document buttons defined in the workflow graph 455 * 456 * @since 5.6 457 */ 458 @SuppressWarnings("boxing") 459 public List<Action> getTaskActions(String selectionListName) { 460 Map<String, Action> actions = new LinkedHashMap<String, Action>(); 461 Map<String, Map<String, Action>> actionsPerTaskType = new LinkedHashMap<String, Map<String, Action>>(); 462 Map<String, Integer> actionsCounter = new HashMap<String, Integer>(); 463 List<DocumentModel> docs = documentsListsManager.getWorkingList(selectionListName); 464 boolean cachePerType = Boolean.TRUE.equals(Boolean.valueOf(Framework.getProperty(CACHE_ACTIONS_PER_TASK_TYPE_PROP_NAME))); 465 int taskDocsNum = 0; 466 if (docs != null && !docs.isEmpty()) { 467 for (DocumentModel doc : docs) { 468 if (doc.hasFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME)) { 469 Task task = new TaskImpl(doc); 470 String taskType = task.getType(); 471 Map<String, Action> taskActions = Collections.emptyMap(); 472 // if caching per type, fill the per type map, else update 473 // actions directly 474 if (cachePerType) { 475 if (actionsPerTaskType.containsKey(taskType)) { 476 taskActions = actionsPerTaskType.get(taskType); 477 } else { 478 taskActions = getTaskActionsMap(task); 479 actionsPerTaskType.put(taskType, taskActions); 480 } 481 } else { 482 taskActions = getTaskActionsMap(task); 483 actions.putAll(taskActions); 484 } 485 for (String actionId : taskActions.keySet()) { 486 Integer count = actionsCounter.get(actionId); 487 if (count == null) { 488 actionsCounter.put(actionId, 1); 489 } else { 490 actionsCounter.put(actionId, count + 1); 491 } 492 } 493 taskDocsNum++; 494 } 495 } 496 } 497 if (cachePerType) { 498 // initialize actions for cache map 499 for (Map<String, Action> actionsPerType : actionsPerTaskType.values()) { 500 actions.putAll(actionsPerType); 501 } 502 } 503 List<Action> res = new ArrayList<Action>(actions.values()); 504 for (Action action : res) { 505 if (!actionsCounter.get(action.getId()).equals(taskDocsNum)) { 506 action.setAvailable(false); 507 } 508 } 509 return res; 510 } 511 512 /** 513 * Ends a task given a selection list name and an action 514 * 515 * @since 5.6 516 */ 517 @SuppressWarnings("unchecked") 518 public String endTasks(String selectionListName, Action taskAction) { 519 // collect form data 520 Map<String, Object> data = new HashMap<String, Object>(); 521 String buttonId = (String) taskAction.getProperties().get("buttonId"); 522 Map<String, Serializable> formVariables = (Map<String, Serializable>) taskAction.getProperties().get( 523 "formVariables"); 524 if (formVariables != null) { 525 data.put("WorkflowVariables", formVariables); 526 data.put("NodeVariables", formVariables); 527 // if there is a comment on the submitted form, pass it to be 528 // logged by audit 529 if (formVariables.containsKey("comment")) { 530 data.put("comment", formVariables.get("comment")); 531 } 532 } 533 534 // get task documents 535 boolean hasErrors = false; 536 DocumentRoutingService routing = Framework.getLocalService(DocumentRoutingService.class); 537 List<DocumentModel> docs = documentsListsManager.getWorkingList(selectionListName); 538 if (docs != null && !docs.isEmpty()) { 539 for (DocumentModel doc : docs) { 540 if (doc.hasFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME)) { 541 // add the button name that was clicked 542 try { 543 routing.endTask(documentManager, new TaskImpl(doc), data, buttonId); 544 } catch (DocumentRouteException e) { 545 log.error(e, e); 546 hasErrors = true; 547 } 548 } 549 } 550 } 551 if (hasErrors) { 552 facesMessages.add(StatusMessage.Severity.ERROR, messages.get("workflow.feedback.error.tasksEnded")); 553 } else { 554 facesMessages.add(StatusMessage.Severity.INFO, messages.get("workflow.feedback.info.tasksEnded")); 555 } 556 // reset selection list 557 documentsListsManager.resetWorkingList(selectionListName); 558 // raise document change event to trigger refresh of content views 559 // listing task documents. 560 Events.instance().raiseEvent(EventNames.DOCUMENT_CHANGED); 561 Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED); 562 return null; 563 } 564 565 private ActionManager getActionService() { 566 if (actionService == null) { 567 actionService = Framework.getLocalService(ActionManager.class); 568 } 569 return actionService; 570 } 571 572 /*** 573 * @since 5.7 574 */ 575 @Observer(value = { TaskEventNames.WORKFLOW_TASK_COMPLETED, TaskEventNames.WORKFLOW_TASK_REASSIGNED, 576 TaskEventNames.WORKFLOW_TASK_DELEGATED }) 577 @BypassInterceptors 578 public void OnTaskCompleted() { 579 if (contentViewActions != null) { 580 contentViewActions.refreshOnSeamEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED); 581 contentViewActions.resetPageProviderOnSeamEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED); 582 } 583 tasksInfoCache.clear(); 584 currentTask = null; 585 } 586 587 /** 588 * @since 5.7.3 589 */ 590 public String reassignTask(TaskInfo taskInfo) { 591 try { 592 Framework.getLocalService(DocumentRoutingService.class).reassignTask(documentManager, taskInfo.getTaskId(), 593 taskInfo.getActors(), taskInfo.getComment()); 594 Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_REASSIGNED); 595 } catch (DocumentRouteException e) { 596 log.error(e); 597 facesMessages.add(StatusMessage.Severity.ERROR, messages.get("workflow.feedback.error.taskEnded")); 598 } 599 return null; 600 } 601 602 /** 603 * @since 5.7.3 604 */ 605 public String getWorkflowTitle(String instanceId) { 606 String workflowTitle = ""; 607 608 try { 609 DocumentModel routeInstance = documentManager.getDocument(new IdRef(instanceId)); 610 workflowTitle = routeInstance.getTitle(); 611 } catch (DocumentNotFoundException e) { 612 log.error("Can not fetch route instance with id " + instanceId, e); 613 } 614 return workflowTitle; 615 } 616 617 /** 618 * @since 5.8 619 */ 620 public String delegateTask(TaskInfo taskInfo) { 621 try { 622 Framework.getLocalService(DocumentRoutingService.class).delegateTask(documentManager, taskInfo.getTaskId(), 623 taskInfo.getActors(), taskInfo.getComment()); 624 Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_DELEGATED); 625 } catch (DocumentRouteException e) { 626 log.error(e); 627 facesMessages.add(StatusMessage.Severity.ERROR, messages.get("workflow.feedback.error.taskEnded")); 628 } 629 return null; 630 } 631 632 /** 633 * @since 5.8 634 */ 635 public String navigateToTask(DocumentModel taskDoc) { 636 setCurrentTask(taskDoc.getAdapter(Task.class)); 637 return null; 638 } 639 640 /** 641 * @since 5.8 642 */ 643 public String navigateToTasksView() { 644 setCurrentTask(null); 645 return null; 646 } 647 648 /** 649 * @since 5.8 650 */ 651 public Task getCurrentTask() { 652 return currentTask; 653 } 654 655 /** 656 * @since 5.8 657 */ 658 public void setCurrentTask(Task currentTask) { 659 this.currentTask = currentTask; 660 } 661 662 /** 663 * Added to avoid an error when opening a task created @before 5.8 see NXP-14047 664 * 665 * @since 5.9.3, 5.8.0-HF10 666 * @return 667 */ 668 @SuppressWarnings("deprecation") 669 public List<String> getCurrentTaskTargetDocumentsIds() { 670 Set<String> uniqueTargetDocIds = new HashSet<String>(); 671 List<String> docIds = new ArrayList<String>(); 672 if (currentTask == null) { 673 return docIds; 674 } 675 uniqueTargetDocIds.addAll(currentTask.getTargetDocumentsIds()); 676 docIds.addAll(uniqueTargetDocIds); 677 return docIds.isEmpty() ? null : docIds; 678 } 679 680 /** 681 * @since 5.8 - Define if action reassign task can be displayed. 682 */ 683 public boolean canBeReassign() { 684 if (currentTask == null) { 685 return false; 686 } 687 DocumentModel workflowInstance = documentManager.getDocument(new IdRef(currentTask.getProcessId())); 688 GraphRoute workflow = workflowInstance.getAdapter(GraphRoute.class); 689 if (workflow == null) { 690 return false; 691 } 692 GraphNode node = workflow.getNode(currentTask.getType()); 693 return node.allowTaskReassignment() && !currentTask.getDelegatedActors().contains(documentManager.getPrincipal().getName()); 694 } 695}