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