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.NuxeoPrincipal; 063import org.nuxeo.ecm.core.api.UnrestrictedSessionRunner; 064import org.nuxeo.ecm.core.api.security.SecurityConstants; 065import org.nuxeo.ecm.platform.actions.Action; 066import org.nuxeo.ecm.platform.actions.ActionContext; 067import org.nuxeo.ecm.platform.actions.ELActionContext; 068import org.nuxeo.ecm.platform.actions.ejb.ActionManager; 069import org.nuxeo.ecm.platform.contentview.seam.ContentViewActions; 070import org.nuxeo.ecm.platform.forms.layout.api.BuiltinModes; 071import org.nuxeo.ecm.platform.forms.layout.api.LayoutDefinition; 072import org.nuxeo.ecm.platform.forms.layout.api.LayoutRowDefinition; 073import org.nuxeo.ecm.platform.forms.layout.api.WidgetDefinition; 074import org.nuxeo.ecm.platform.forms.layout.api.WidgetReference; 075import org.nuxeo.ecm.platform.forms.layout.service.WebLayoutManager; 076import org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants; 077import org.nuxeo.ecm.platform.routing.api.DocumentRoutingService; 078import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteException; 079import org.nuxeo.ecm.platform.routing.core.impl.GraphNode; 080import org.nuxeo.ecm.platform.routing.core.impl.GraphNode.Button; 081import org.nuxeo.ecm.platform.routing.core.impl.GraphRoute; 082import org.nuxeo.ecm.platform.task.Task; 083import org.nuxeo.ecm.platform.task.TaskEventNames; 084import org.nuxeo.ecm.platform.task.TaskImpl; 085import org.nuxeo.ecm.platform.ui.web.api.NavigationContext; 086import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; 087import org.nuxeo.ecm.webapp.action.ActionContextProvider; 088import org.nuxeo.ecm.webapp.documentsLists.DocumentsListsManager; 089import org.nuxeo.ecm.webapp.helpers.EventNames; 090import org.nuxeo.runtime.api.Framework; 091 092/** 093 * Task validators 094 * 095 * @since 5.6 096 */ 097@Scope(CONVERSATION) 098@Name("routingTaskActions") 099public class RoutingTaskActionsBean implements Serializable { 100 101 private static final long serialVersionUID = 1L; 102 103 private static final Log log = LogFactory.getLog(RoutingTaskActionsBean.class); 104 105 public static final String SUBJECT_PATTERN = "([a-zA-Z_0-9]*(:)[a-zA-Z_0-9]*)"; 106 107 /** 108 * Runtime property name, that makes it possible to cache actions available on a given task, depending on its type. 109 * <p> 110 * This caching is global to all tasks in the platform, and will not work correctly if some tasks are filtering some 111 * actions depending on local variables, for instance. 112 * 113 * @since 5.7 114 */ 115 public static final String CACHE_ACTIONS_PER_TASK_TYPE_PROP_NAME = "org.nuxeo.routing.cacheActionsPerTaskType"; 116 117 @In(create = true, required = false) 118 protected transient CoreSession documentManager; 119 120 @In(required = true, create = true) 121 protected NavigationContext navigationContext; 122 123 @In(create = true, required = false) 124 protected FacesMessages facesMessages; 125 126 @In(create = true) 127 protected Map<String, String> messages; 128 129 @In(create = true) 130 protected transient DocumentsListsManager documentsListsManager; 131 132 @In(create = true, required = false) 133 protected transient ActionContextProvider actionContextProvider; 134 135 @In(create = true, required = false) 136 protected ContentViewActions contentViewActions; 137 138 @RequestParameter("button") 139 protected String button; 140 141 protected ActionManager actionService; 142 143 protected Map<String, TaskInfo> tasksInfoCache = new HashMap<String, TaskInfo>(); 144 145 protected Task currentTask; 146 147 protected List<String> formVariablesToRemove; 148 149 public void validateTaskDueDate(FacesContext context, UIComponent component, Object value) { 150 final String DATE_FORMAT = "dd/MM/yyyy"; 151 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); 152 153 String messageString = null; 154 if (value != null) { 155 try { 156 Date dueDate = dateFormat.parse(dateFormat.format((Date) value)); 157 Date today = dateFormat.parse(dateFormat.format(new Date())); 158 if (dueDate.before(today)) { 159 messageString = "label.workflow.error.outdated_duedate"; 160 } 161 } catch (ParseException e) { 162 messageString = "label.workflow.error.date_parsing"; 163 } 164 } 165 166 if (messageString != null) { 167 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context, 168 "label.workflow.error.outdated_duedate"), null); 169 ((EditableValueHolder) component).setValid(false); 170 context.addMessage(component.getClientId(context), message); 171 } 172 } 173 174 public void validateSubject(FacesContext context, UIComponent component, Object value) { 175 if (!((value instanceof String) && ((String) value).matches(SUBJECT_PATTERN))) { 176 FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, ComponentUtils.translate(context, 177 "label.document.routing.invalid.subject"), null); 178 context.addMessage(null, message); 179 throw new ValidatorException(message); 180 } 181 } 182 183 public String getTaskLayout(Task task) { 184 return getTaskInfo(task, true).layout; 185 } 186 187 public List<Action> getTaskButtons(Task task) { 188 List<Button> buttons = getTaskInfo(task, true).buttons; 189 List<Action> actions = new ArrayList<Action>(); 190 191 DocumentModel workflowInstance = documentManager.getDocument(new IdRef(task.getProcessId())); 192 GraphRoute workflow = workflowInstance.getAdapter(GraphRoute.class); 193 if (workflow == null) { 194 // task was not created by a workflow process , no actions to 195 // display; 196 return actions; 197 } 198 GraphNode node = workflow.getNode(task.getType()); 199 for (Button button : buttons) { 200 Action action = new Action(button.getName(), Action.EMPTY_CATEGORIES); 201 action.setLabel(button.getLabel()); 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 228 // by audit 229 if (formVariables.containsKey(GraphNode.NODE_VARIABLE_COMMENT)) { 230 data.put(GraphNode.NODE_VARIABLE_COMMENT, formVariables.get(GraphNode.NODE_VARIABLE_COMMENT)); 231 } 232 } 233 // add the button name that was clicked 234 try { 235 DocumentRoutingService routing = Framework.getService(DocumentRoutingService.class); 236 routing.endTask(documentManager, task, data, button); 237 facesMessages.add(StatusMessage.Severity.INFO, messages.get("workflow.feedback.info.taskEnded")); 238 } catch (DocumentRouteException e) { 239 log.error(e, e); 240 facesMessages.add(StatusMessage.Severity.ERROR, messages.get("workflow.feedback.error.taskEnded")); 241 } 242 Events.instance().raiseEvent(TaskEventNames.WORKFLOW_TASK_COMPLETED); 243 clear(task.getId()); 244 if (navigationContext.getCurrentDocument() != null 245 && documentManager.hasPermission(navigationContext.getCurrentDocument().getRef(), 246 SecurityConstants.READ)) { 247 return null; 248 } 249 // if the user only had temporary permissions on the current doc given 250 // by the workflow 251 navigationContext.setCurrentDocument(null); 252 return navigationContext.goHome(); 253 } 254 255 private void clear(String taskId) { 256 button = null; 257 if (tasksInfoCache.containsKey(taskId)) { 258 tasksInfoCache.remove(taskId); 259 } 260 } 261 262 public Map<String, Serializable> getFormVariables(Task task) { 263 return getTaskInfo(task, true).formVariables; 264 } 265 266 public class TaskInfo { 267 protected HashMap<String, Serializable> formVariables; 268 269 protected String layout; 270 271 protected boolean canBeReassigned; 272 273 protected List<Button> buttons; 274 275 protected List<String> actors; 276 277 protected String comment; 278 279 protected String taskId; 280 281 protected String name; 282 283 protected TaskInfo(String taskId, HashMap<String, Serializable> formVariables, String layout, 284 List<Button> buttons, boolean canBeReassigned, String name) { 285 this.formVariables = formVariables; 286 this.layout = layout; 287 this.buttons = buttons; 288 this.canBeReassigned = canBeReassigned; 289 this.taskId = taskId; 290 this.name = name; 291 } 292 293 public List<String> getActors() { 294 return actors; 295 } 296 297 public void setActors(List<String> actors) { 298 this.actors = actors; 299 } 300 301 public String getComment() { 302 return comment; 303 } 304 305 public void setComment(String comment) { 306 this.comment = comment; 307 } 308 309 public boolean isCanBeReassigned() { 310 return canBeReassigned; 311 } 312 313 public String getTaskId() { 314 return taskId; 315 } 316 317 public String getName() { 318 return name; 319 } 320 } 321 322 // we have to be unrestricted to get this info 323 // because the current user may not be the one that started the 324 // workflow 325 public TaskInfo getTaskInfo(final Task task, final boolean getFormVariables) { 326 if (tasksInfoCache.containsKey(task.getId())) { 327 return tasksInfoCache.get(task.getId()); 328 } 329 final String routeDocId = task.getVariable(DocumentRoutingConstants.TASK_ROUTE_INSTANCE_DOCUMENT_ID_KEY); 330 final String nodeId = task.getVariable(DocumentRoutingConstants.TASK_NODE_ID_KEY); 331 if (routeDocId == null) { 332 throw new NuxeoException("Can not get the source graph for this task"); 333 } 334 if (nodeId == null) { 335 throw new NuxeoException("Can not get the source node for this task"); 336 } 337 final TaskInfo[] res = new TaskInfo[1]; 338 new UnrestrictedSessionRunner(documentManager) { 339 @Override 340 public void run() { 341 DocumentModel doc = session.getDocument(new IdRef(routeDocId)); 342 GraphRoute route = doc.getAdapter(GraphRoute.class); 343 GraphNode node = route.getNode(nodeId); 344 HashMap<String, Serializable> map = new HashMap<String, Serializable>(); 345 if (getFormVariables) { 346 map.putAll(node.getVariables()); 347 map.putAll(route.getVariables()); 348 } 349 res[0] = new TaskInfo(task.getId(), map, node.getTaskLayout(), node.getTaskButtons(), 350 node.allowTaskReassignment(), task.getName()); 351 } 352 }.runUnrestricted(); 353 // don't add tasks in cache when are fetched without the form variables 354 // for 355 // bulk processing 356 if (getFormVariables) { 357 tasksInfoCache.put(task.getId(), res[0]); 358 } 359 return res[0]; 360 } 361 362 /** 363 * @since 5.6 364 */ 365 public boolean isRoutingTask(Task task) { 366 return task.getDocument().hasFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME); 367 } 368 369 /** 370 * @since 5.6 371 */ 372 public List<Action> getTaskActions(Task task) { 373 return new ArrayList<Action>(getTaskActionsMap(task).values()); 374 } 375 376 // temp method because Studio also refers to empty layouts 377 protected boolean isLayoutEmpty(String layoutName) { 378 if (layoutName == null || layoutName.isEmpty()) { 379 return true; 380 } 381 // explore the layout and find out if it contains only empty widgets 382 WebLayoutManager lm = Framework.getService(WebLayoutManager.class); 383 LayoutDefinition layout = lm.getLayoutDefinition(layoutName); 384 if (layout == null || layout.isEmpty()) { 385 return true; 386 } 387 return false; 388 } 389 390 /** 391 * Helper to generate a unique action id for all task types 392 * 393 * @since 5.7 394 */ 395 protected String getTaskActionId(Task task, String buttonId) { 396 return String.format("%s_%s", task.getType(), buttonId); 397 } 398 399 /** 400 * @since 5.6 401 */ 402 public Map<String, Action> getTaskActionsMap(Task task) { 403 Map<String, Action> actions = new LinkedHashMap<String, Action>(); 404 TaskInfo taskInfo = getTaskInfo(task, true); 405 String layout = taskInfo.layout; 406 List<Button> buttons = taskInfo.buttons; 407 408 boolean addLayout = !isLayoutEmpty(layout); 409 Map<String, Serializable> props = null; 410 if (addLayout) { 411 props = new HashMap<String, Serializable>(); 412 props.put("layout", layout); 413 props.put("formVariables", taskInfo.formVariables); 414 } 415 416 if (buttons != null && !buttons.isEmpty()) { 417 for (Button button : buttons) { 418 String buttonId = button.getName(); 419 String id = getTaskActionId(task, buttonId); 420 Action action = new Action(id, Action.EMPTY_CATEGORIES); 421 action.setLabel(button.getLabel()); 422 Map<String, Serializable> actionProps = new HashMap<String, Serializable>(); 423 actionProps.put("buttonId", buttonId); 424 if (addLayout) { 425 actionProps.putAll(props); 426 action.setProperties(actionProps); 427 action.setType("fancybox"); 428 } else { 429 action.setProperties(actionProps); 430 action.setType("link"); 431 } 432 boolean displayAction = true; 433 if (StringUtils.isNotEmpty(button.getFilter())) { 434 displayAction = getActionService().checkFilter(button.filter, 435 actionContextProvider.createActionContext()); 436 } 437 if (displayAction) { 438 actions.put(id, action); 439 } 440 } 441 } 442 443 // If there is a form attached to these tasks, add a generic 444 // process action to open the fancy box. 445 // The form of the first task will be displayed, but all the tasks 446 // concerned by this action share the same form, as they share the 447 // same type. 448 if (addLayout && !actions.isEmpty()) { 449 String id = getTaskActionId(task, "process_task"); 450 Action processAction = new Action(id, Action.EMPTY_CATEGORIES); 451 452 formVariablesToRemove = new ArrayList<>(); 453 WebLayoutManager layoutService = Framework.getService(WebLayoutManager.class); 454 LayoutDefinition taskLayout = layoutService.getLayoutDefinition(taskInfo.layout); 455 if (taskLayout != null) { 456 for (LayoutRowDefinition row : taskLayout.getRows()) { 457 for (WidgetReference widgetRef : row.getWidgetReferences()) { 458 WidgetDefinition widgetDefinition = taskLayout.getWidgetDefinition(widgetRef.getName()); 459 if (widgetDefinition == null) { 460 continue; 461 } 462 463 String mode = widgetDefinition.getMode(BuiltinModes.EDIT); 464 ActionContext el = new ELActionContext(); 465 el.setCurrentPrincipal((NuxeoPrincipal) documentManager.getPrincipal()); 466 el.setCurrentDocument(navigationContext.getCurrentDocument()); 467 mode = el.evalExpression(mode, String.class); 468 if (mode != null && !mode.equals(BuiltinModes.EDIT)) { 469 Arrays.stream(widgetDefinition.getFieldDefinitions()).forEach((field) -> { 470 // workflow form fields are always like "['$variable']" 471 // remove both [' and '] to keep only the variable name 472 String fieldName = field.getFieldName().replaceAll("^\\['|']$", ""); 473 formVariablesToRemove.add(fieldName); 474 }); 475 } 476 } 477 } 478 } 479 480 processAction.setProperties(props); 481 processAction.setType("process_task"); 482 actions.put(id, processAction); 483 } 484 485 return actions; 486 } 487 488 /** 489 * Returns actions for task document buttons defined in the workflow graph 490 * 491 * @since 5.6 492 */ 493 @SuppressWarnings("boxing") 494 public List<Action> getTaskActions(String selectionListName) { 495 Map<String, Action> actions = new LinkedHashMap<String, Action>(); 496 Map<String, Map<String, Action>> actionsPerTaskType = new LinkedHashMap<String, Map<String, Action>>(); 497 Map<String, Integer> actionsCounter = new HashMap<String, Integer>(); 498 List<DocumentModel> docs = documentsListsManager.getWorkingList(selectionListName); 499 boolean cachePerType = Boolean.TRUE.equals(Boolean.valueOf(Framework.getProperty(CACHE_ACTIONS_PER_TASK_TYPE_PROP_NAME))); 500 int taskDocsNum = 0; 501 if (docs != null && !docs.isEmpty()) { 502 for (DocumentModel doc : docs) { 503 if (doc.hasFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME)) { 504 Task task = new TaskImpl(doc); 505 String taskType = task.getType(); 506 Map<String, Action> taskActions = Collections.emptyMap(); 507 // if caching per type, fill the per type map, else update 508 // actions directly 509 if (cachePerType) { 510 if (actionsPerTaskType.containsKey(taskType)) { 511 taskActions = actionsPerTaskType.get(taskType); 512 } else { 513 taskActions = getTaskActionsMap(task); 514 actionsPerTaskType.put(taskType, taskActions); 515 } 516 } else { 517 taskActions = getTaskActionsMap(task); 518 actions.putAll(taskActions); 519 } 520 for (String actionId : taskActions.keySet()) { 521 Integer count = actionsCounter.get(actionId); 522 if (count == null) { 523 actionsCounter.put(actionId, 1); 524 } else { 525 actionsCounter.put(actionId, count + 1); 526 } 527 } 528 taskDocsNum++; 529 } 530 } 531 } 532 if (cachePerType) { 533 // initialize actions for cache map 534 for (Map<String, Action> actionsPerType : actionsPerTaskType.values()) { 535 actions.putAll(actionsPerType); 536 } 537 } 538 List<Action> res = new ArrayList<Action>(actions.values()); 539 for (Action action : res) { 540 if (!actionsCounter.get(action.getId()).equals(taskDocsNum)) { 541 action.setAvailable(false); 542 } 543 } 544 return res; 545 } 546 547 /** 548 * Ends a task given a selection list name and an action 549 * 550 * @since 5.6 551 */ 552 @SuppressWarnings("unchecked") 553 public String endTasks(String selectionListName, Action taskAction) { 554 // collect form data 555 Map<String, Object> data = new HashMap<String, Object>(); 556 String buttonId = (String) taskAction.getProperties().get("buttonId"); 557 Map<String, Serializable> formVariables = (Map<String, Serializable>) taskAction.getProperties().get( 558 "formVariables"); 559 560 // Remove form variables that should not be updated 561 if (formVariablesToRemove != null && formVariables != null) { 562 formVariablesToRemove.forEach(formVariables::remove); 563 } 564 formVariablesToRemove = null; 565 566 if (formVariables != null && !formVariables.isEmpty()) { 567 data.put("WorkflowVariables", formVariables); 568 data.put("NodeVariables", formVariables); 569 // if there is a comment on the submitted form, pass it to be 570 // logged by audit 571 if (formVariables.containsKey(GraphNode.NODE_VARIABLE_COMMENT)) { 572 data.put(GraphNode.NODE_VARIABLE_COMMENT, formVariables.get(GraphNode.NODE_VARIABLE_COMMENT)); 573 } 574 } 575 576 // get task documents 577 boolean hasErrors = false; 578 DocumentRoutingService routing = Framework.getService(DocumentRoutingService.class); 579 List<DocumentModel> docs = documentsListsManager.getWorkingList(selectionListName); 580 if (docs != null && !docs.isEmpty()) { 581 for (DocumentModel doc : docs) { 582 if (doc.hasFacet(DocumentRoutingConstants.ROUTING_TASK_FACET_NAME)) { 583 // add the button name that was clicked 584 try { 585 routing.endTask(documentManager, new TaskImpl(doc), data, buttonId); 586 } catch (DocumentRouteException e) { 587 log.error(e, e); 588 hasErrors = true; 589 } 590 } 591 } 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}