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}