001/* 002 * (C) Copyright 2012-2018 Nuxeo (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 * Florent Guillaume 018 */ 019package org.nuxeo.ecm.platform.routing.core.impl; 020 021import java.io.Serializable; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Calendar; 025import java.util.Collections; 026import java.util.Date; 027import java.util.HashMap; 028import java.util.List; 029import java.util.Map; 030import java.util.Map.Entry; 031 032import org.apache.commons.lang3.StringUtils; 033import org.apache.commons.lang3.builder.ToStringBuilder; 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.nuxeo.ecm.automation.AutomationService; 037import org.nuxeo.ecm.automation.OperationContext; 038import org.nuxeo.ecm.automation.OperationException; 039import org.nuxeo.ecm.automation.core.Constants; 040import org.nuxeo.ecm.automation.core.scripting.DateWrapper; 041import org.nuxeo.ecm.automation.core.scripting.Expression; 042import org.nuxeo.ecm.automation.core.scripting.Scripting; 043import org.nuxeo.ecm.core.api.CoreSession; 044import org.nuxeo.ecm.core.api.DocumentModel; 045import org.nuxeo.ecm.core.api.DocumentModelList; 046import org.nuxeo.ecm.core.api.DocumentRef; 047import org.nuxeo.ecm.core.api.IdRef; 048import org.nuxeo.ecm.core.api.NuxeoException; 049import org.nuxeo.ecm.core.api.NuxeoPrincipal; 050import org.nuxeo.ecm.core.api.impl.DocumentModelImpl; 051import org.nuxeo.ecm.core.api.model.Property; 052import org.nuxeo.ecm.core.api.model.impl.ListProperty; 053import org.nuxeo.ecm.core.api.model.impl.MapProperty; 054import org.nuxeo.ecm.core.api.validation.DocumentValidationException; 055import org.nuxeo.ecm.core.api.validation.DocumentValidationReport; 056import org.nuxeo.ecm.core.api.validation.DocumentValidationService; 057import org.nuxeo.ecm.core.schema.SchemaManager; 058import org.nuxeo.ecm.core.schema.types.CompositeType; 059import org.nuxeo.ecm.core.schema.types.Schema; 060import org.nuxeo.ecm.core.schema.utils.DateParser; 061import org.nuxeo.ecm.platform.routing.api.DocumentRoute; 062import org.nuxeo.ecm.platform.routing.api.DocumentRoutingConstants; 063import org.nuxeo.ecm.platform.routing.api.DocumentRoutingService; 064import org.nuxeo.ecm.platform.routing.api.exception.DocumentRouteException; 065import org.nuxeo.ecm.platform.routing.core.api.TasksInfoWrapper; 066import org.nuxeo.ecm.platform.routing.core.api.scripting.RoutingScriptingExpression; 067import org.nuxeo.ecm.platform.routing.core.api.scripting.RoutingScriptingFunctions; 068import org.nuxeo.ecm.platform.task.Task; 069import org.nuxeo.ecm.platform.task.TaskConstants; 070import org.nuxeo.runtime.api.Framework; 071 072/** 073 * Graph Node implementation as an adapter over a DocumentModel. 074 * 075 * @since 5.6 076 */ 077public class GraphNodeImpl extends DocumentRouteElementImpl implements GraphNode { 078 079 private static final long serialVersionUID = 1L; 080 081 private static final Log log = LogFactory.getLog(GraphNodeImpl.class); 082 083 private static final String EXPR_PREFIX = "expr:"; 084 085 private static final String TEMPLATE_START = "@{"; 086 087 protected final GraphRouteImpl graph; 088 089 protected State localState; 090 091 /** To be used through getter. */ 092 protected List<Transition> inputTransitions; 093 094 /** To be used through getter. */ 095 protected List<Transition> outputTransitions; 096 097 /** To be used through getter. */ 098 protected List<Button> taskButtons; 099 100 protected List<EscalationRule> escalationRules; 101 102 protected List<TaskInfo> tasksInfo; 103 104 public GraphNodeImpl(DocumentModel doc, GraphRouteImpl graph) { 105 super(doc, new GraphRunner()); 106 this.graph = graph; 107 inputTransitions = new ArrayList<>(2); 108 } 109 110 /** 111 * @since 5.7.2 112 */ 113 public GraphNodeImpl(DocumentModel doc) { 114 super(doc, new GraphRunner()); 115 graph = (GraphRouteImpl) getDocumentRoute(doc.getCoreSession()); 116 inputTransitions = new ArrayList<>(2); 117 } 118 119 @Override 120 public String toString() { 121 return new ToStringBuilder(this).append(getId()).toString(); 122 } 123 124 protected boolean getBoolean(String propertyName) { 125 return Boolean.TRUE.equals(getProperty(propertyName)); 126 } 127 128 protected void incrementProp(String prop) { 129 Long count = (Long) getProperty(prop); 130 if (count == null) { 131 count = Long.valueOf(0); 132 } 133 document.setPropertyValue(prop, Long.valueOf(count.longValue() + 1)); 134 saveDocument(); 135 } 136 137 protected CoreSession getSession() { 138 return document.getCoreSession(); 139 } 140 141 protected void saveDocument() { 142 getSession().saveDocument(document); 143 } 144 145 @Override 146 public String getId() { 147 return (String) getProperty(PROP_NODE_ID); 148 } 149 150 @Override 151 public State getState() { 152 if (localState != null) { 153 return localState; 154 } 155 String s = document.getCurrentLifeCycleState(); 156 return State.fromString(s); 157 } 158 159 @Override 160 public void setState(State state) { 161 if (state == null) { 162 throw new NullPointerException("null state"); 163 } 164 String lc = state.getLifeCycleState(); 165 if (lc == null) { 166 localState = state; 167 } else { 168 localState = null; 169 String oldLc = document.getCurrentLifeCycleState(); 170 if (lc.equals(oldLc)) { 171 return; 172 } 173 document.followTransition(state.getTransition()); 174 saveDocument(); 175 } 176 } 177 178 @Override 179 public boolean isStart() { 180 return getBoolean(PROP_START); 181 } 182 183 @Override 184 public boolean isStop() { 185 return getBoolean(PROP_STOP); 186 } 187 188 @Override 189 public void setCanceled() { 190 log.debug("Canceling " + this); 191 incrementProp(PROP_CANCELED); 192 } 193 194 @Override 195 public long getCanceledCount() { 196 Long c = (Long) getProperty(PROP_CANCELED); 197 return c == null ? 0 : c.longValue(); 198 } 199 200 @Override 201 public boolean isMerge() { 202 String merge = (String) getProperty(PROP_MERGE); 203 return StringUtils.isNotEmpty(merge); 204 } 205 206 @Override 207 public String getInputChain() { 208 return (String) getProperty(PROP_INPUT_CHAIN); 209 } 210 211 @Override 212 public String getOutputChain() { 213 return (String) getProperty(PROP_OUTPUT_CHAIN); 214 } 215 216 @Override 217 public boolean hasTask() { 218 return getBoolean(PROP_HAS_TASK); 219 } 220 221 @SuppressWarnings("unchecked") 222 @Override 223 public List<String> getTaskAssignees() { 224 return (List<String>) getProperty(PROP_TASK_ASSIGNEES); 225 } 226 227 public String getTaskAssigneesVar() { 228 return (String) getProperty(PROP_TASK_ASSIGNEES_VAR); 229 } 230 231 @Override 232 public Date getTaskDueDate() { 233 Calendar cal = (Calendar) getProperty(PROP_TASK_DUE_DATE); 234 return cal == null ? null : cal.getTime(); 235 } 236 237 @Override 238 public String getTaskDirective() { 239 return (String) getProperty(PROP_TASK_DIRECTIVE); 240 } 241 242 @Override 243 public String getTaskAssigneesPermission() { 244 return (String) getProperty(PROP_TASK_ASSIGNEES_PERMISSION); 245 } 246 247 @Override 248 public String getTaskLayout() { 249 return (String) getProperty(PROP_TASK_LAYOUT); 250 } 251 252 @Override 253 public String getTaskNotificationTemplate() { 254 return (String) getProperty(PROP_TASK_NOTIFICATION_TEMPLATE); 255 } 256 257 @Override 258 public String getTaskDueDateExpr() { 259 return (String) getProperty(PROP_TASK_DUE_DATE_EXPR); 260 } 261 262 @Override 263 public void starting() { 264 // Allow input transitions reevaluation (needed for loop case) 265 for (Transition t : inputTransitions) { 266 t.setResult(false); 267 getSession().saveDocument(t.source.getDocument()); 268 } 269 // Increment node counter 270 incrementProp(PROP_COUNT); 271 document.setPropertyValue(PROP_NODE_START_DATE, Calendar.getInstance()); 272 // reset taskInfo property 273 tasksInfo = null; 274 document.setPropertyValue(PROP_TASKS_INFO, new ArrayList<TaskInfo>()); 275 saveDocument(); 276 } 277 278 @Override 279 public void ending() { 280 document.setPropertyValue(PROP_NODE_END_DATE, Calendar.getInstance()); 281 saveDocument(); 282 } 283 284 @Override 285 public Map<String, Serializable> getVariables() { 286 return GraphVariablesUtil.getVariables(document, PROP_VARIABLES_FACET); 287 } 288 289 @Override 290 public Map<String, Serializable> getJsonVariables() { 291 return GraphVariablesUtil.getVariables(document, PROP_VARIABLES_FACET, true); 292 } 293 294 @Override 295 public void setVariables(Map<String, Serializable> map) { 296 GraphVariablesUtil.setVariables(document, PROP_VARIABLES_FACET, map); 297 } 298 299 @Override 300 public void setJSONVariables(Map<String, String> map) { 301 GraphVariablesUtil.setJSONVariables(document, PROP_VARIABLES_FACET, map); 302 } 303 304 @SuppressWarnings("unchecked") 305 @Override 306 public void setAllVariables(Map<String, Object> map) { 307 setAllVariables(map, true); 308 } 309 310 @SuppressWarnings("unchecked") 311 @Override 312 public void setAllVariables(Map<String, Object> map, final boolean allowGlobalVariablesAssignement) { 313 if (map == null) { 314 return; 315 } 316 Boolean mapToJSON = Boolean.FALSE; 317 if (map.containsKey(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON) 318 && (Boolean) map.get(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON)) { 319 mapToJSON = Boolean.TRUE; 320 } 321 322 // get variables from node and graph 323 Map<String, Serializable> graphVariables = mapToJSON ? graph.getJsonVariables() : graph.getVariables(); 324 Map<String, Serializable> nodeVariables = mapToJSON ? getJsonVariables() : getVariables(); 325 Map<String, Serializable> changedGraphVariables = new HashMap<>(); 326 Map<String, Serializable> changedNodeVariables = new HashMap<>(); 327 328 // set variables back into node and graph 329 if (map.get(Constants.VAR_WORKFLOW_NODE) != null) { 330 for (Entry<String, Serializable> es : ((Map<String, Serializable>) map.get( 331 Constants.VAR_WORKFLOW_NODE)).entrySet()) { 332 String key = es.getKey(); 333 Serializable value = es.getValue(); 334 if (nodeVariables.containsKey(key)) { 335 Serializable oldValue = nodeVariables.get(key); 336 if (!equality(value, oldValue)) { 337 changedNodeVariables.put(key, value); 338 } 339 } 340 } 341 } 342 final String transientSchemaName = DocumentRoutingConstants.GLOBAL_VAR_SCHEMA_PREFIX + getId(); 343 final SchemaManager schemaManager = Framework.getService(SchemaManager.class); 344 if (map.get(Constants.VAR_WORKFLOW) != null) { 345 final Schema transientSchema = schemaManager.getSchema(transientSchemaName); 346 for (Entry<String, Serializable> es : ((Map<String, Serializable>) map.get( 347 Constants.VAR_WORKFLOW)).entrySet()) { 348 String key = es.getKey(); 349 Serializable value = es.getValue(); 350 if (graphVariables.containsKey(key)) { 351 Serializable oldValue = graphVariables.get(key); 352 if (!equality(value, oldValue)) { 353 if (!allowGlobalVariablesAssignement && transientSchema != null 354 && !transientSchema.hasField(key)) { 355 throw new DocumentRouteException(String.format( 356 "You don't have the permission to set the workflow variable %s", key)); 357 } 358 changedGraphVariables.put(key, value); 359 } 360 } 361 } 362 } 363 364 if (!allowGlobalVariablesAssignement) { 365 // Validation 366 final DocumentModel transientDocumentModel = new DocumentModelImpl(getDocument().getType()); 367 transientDocumentModel.copyContent(document); 368 final String transientFacetName = "facet-" + transientSchemaName; 369 CompositeType transientFacet = schemaManager.getFacet(transientFacetName); 370 if (transientFacet != null) { 371 changedGraphVariables.put(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON, mapToJSON); 372 transientDocumentModel.addFacet("facet-" + transientSchemaName); 373 GraphVariablesUtil.setVariables(transientDocumentModel, "facet-" + transientSchemaName, 374 changedGraphVariables, false); 375 } 376 changedNodeVariables.put(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON, mapToJSON); 377 GraphVariablesUtil.setVariables(transientDocumentModel, PROP_VARIABLES_FACET, changedNodeVariables, false); 378 DocumentValidationService documentValidationService = Framework.getService(DocumentValidationService.class); 379 DocumentValidationReport report = documentValidationService.validate(transientDocumentModel); 380 if (report.hasError()) { 381 throw new DocumentValidationException(report); 382 } 383 } 384 385 if (!changedNodeVariables.isEmpty()) { 386 changedNodeVariables.put(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON, mapToJSON); 387 setVariables(changedNodeVariables); 388 } 389 if (!changedGraphVariables.isEmpty()) { 390 changedGraphVariables.put(DocumentRoutingConstants._MAP_VAR_FORMAT_JSON, mapToJSON); 391 graph.setVariables(changedGraphVariables); 392 } 393 } 394 395 public static boolean equality(Object o1, Object o2) { 396 if (o1 == o2) { 397 return true; 398 } 399 if (o1 == null || o2 == null) { 400 return false; 401 } 402 if (o1 instanceof List && o2.getClass().isArray()) { 403 return Arrays.equals(((List<?>) o1).toArray(), (Object[]) o2); 404 } else if (o1.getClass().isArray() && o2 instanceof List) { 405 return Arrays.equals((Object[]) o1, ((List<?>) o2).toArray()); 406 } else if (o1.getClass().isArray() && o2.getClass().isArray()) { 407 // Nuxeo doesn't use arrays of primitive types 408 return Arrays.equals((Object[]) o1, (Object[]) o2); 409 } else { 410 return o1.equals(o2); 411 } 412 } 413 414 protected OperationContext getExecutionContext(CoreSession session) { 415 OperationContext context = new OperationContext(session); 416 context.putAll(getWorkflowContextualInfo(session, true)); 417 context.setCommit(false); // no session save at end 418 DocumentModelList documents = graph.getAttachedDocuments(session); 419 // associated docs 420 context.setInput(documents); 421 return context; 422 } 423 424 @Override 425 public Map<String, Serializable> getWorkflowContextualInfo(CoreSession session, boolean detached) { 426 Map<String, Serializable> context = new HashMap<>(); 427 // workflow context 428 context.put("WorkflowVariables", (Serializable) graph.getVariables()); 429 context.put("workflowInitiator", getWorkflowInitiator()); 430 context.put("workflowStartTime", getWorkflowStartTime()); 431 context.put("workflowParent", getWorkflowParentRouteId()); 432 context.put("workflowParentNode", getWorkflowParentNodeId()); 433 context.put("workflowInstanceId", graph.getDocument().getId()); 434 context.put("taskDueTime", (Calendar) getProperty(PROP_TASK_DUE_DATE)); 435 436 DocumentModelList documents = graph.getAttachedDocuments(session); 437 if (detached) { 438 for (DocumentModel documentModel : documents) { 439 documentModel.detach(true); 440 } 441 } 442 context.put("workflowDocuments", documents); 443 context.put("documents", documents); 444 // node context 445 String button = (String) getProperty(PROP_NODE_BUTTON); 446 Map<String, Serializable> nodeVariables = getVariables(); 447 nodeVariables.put("button", button); 448 nodeVariables.put("numberOfProcessedTasks", getProcessedTasksInfo().size()); 449 nodeVariables.put("numberOfTasks", getTasksInfo().size()); 450 nodeVariables.put("tasks", new TasksInfoWrapper(getTasksInfo())); 451 context.put("NodeVariables", (Serializable) nodeVariables); 452 context.put("nodeId", getId()); 453 String state = getState().name().toLowerCase(); 454 context.put("nodeState", state); 455 context.put("state", state); 456 context.put("nodeStartTime", getNodeStartTime()); 457 context.put("nodeEndTime", getNodeEndTime()); 458 context.put("nodeLastActor", getNodeLastActor()); 459 460 // task context 461 context.put("comment", ""); 462 return context; 463 } 464 465 protected String getWorkflowInitiator() { 466 return (String) graph.getDocument().getPropertyValue(DocumentRoutingConstants.INITIATOR); 467 } 468 469 protected Calendar getWorkflowStartTime() { 470 return (Calendar) graph.getDocument().getPropertyValue("dc:created"); 471 } 472 473 protected String getWorkflowParentRouteId() { 474 return (String) graph.getDocument().getPropertyValue(GraphRoute.PROP_PARENT_ROUTE); 475 } 476 477 protected String getWorkflowParentNodeId() { 478 return (String) graph.getDocument().getPropertyValue(GraphRoute.PROP_PARENT_NODE); 479 } 480 481 protected Calendar getNodeStartTime() { 482 return (Calendar) getDocument().getPropertyValue(PROP_NODE_START_DATE); 483 } 484 485 protected Calendar getNodeEndTime() { 486 return (Calendar) getDocument().getPropertyValue(PROP_NODE_END_DATE); 487 } 488 489 protected String getNodeLastActor() { 490 return (String) getDocument().getPropertyValue(PROP_NODE_LAST_ACTOR); 491 } 492 493 @Override 494 public void executeChain(String chainId) throws DocumentRouteException { 495 executeChain(chainId, null); 496 } 497 498 @Override 499 public void executeTransitionChain(Transition transition) throws DocumentRouteException { 500 executeChain(transition.chain, transition.id); 501 } 502 503 public void executeChain(String chainId, String transitionId) throws DocumentRouteException { 504 // TODO events 505 if (StringUtils.isEmpty(chainId)) { 506 return; 507 } 508 509 // get base context 510 try (OperationContext context = getExecutionContext(getSession())) { 511 if (transitionId != null) { 512 context.put("transition", transitionId); 513 } 514 515 AutomationService automationService = Framework.getService(AutomationService.class); 516 automationService.run(context, chainId); 517 518 setAllVariables(context); 519 } catch (OperationException e) { 520 throw new DocumentRouteException("Error running chain: " + chainId, e); 521 } 522 } 523 524 @Override 525 public void initAddInputTransition(Transition transition) { 526 inputTransitions.add(transition); 527 } 528 529 protected List<Transition> computeOutputTransitions() { 530 ListProperty props = (ListProperty) document.getProperty(PROP_TRANSITIONS); 531 List<Transition> trans = new ArrayList<>(props.size()); 532 for (Property p : props) { 533 trans.add(new Transition(this, p)); 534 } 535 return trans; 536 } 537 538 @Override 539 public List<Transition> getOutputTransitions() { 540 if (outputTransitions == null) { 541 outputTransitions = computeOutputTransitions(); 542 } 543 return outputTransitions; 544 } 545 546 @Override 547 public List<Transition> evaluateTransitions() throws DocumentRouteException { 548 List<Transition> trueTrans = new ArrayList<>(); 549 for (Transition t : getOutputTransitions()) { 550 try (OperationContext context = getExecutionContext(getSession())) { 551 context.put("transition", t.id); 552 Expression expr = new RoutingScriptingExpression(t.condition, new RoutingScriptingFunctions(context)); 553 Object res = expr.eval(context); 554 // stupid eval() method throws generic Exception 555 if (!(res instanceof Boolean)) { 556 throw new DocumentRouteException("Condition for transition " + t + " of node '" + getId() 557 + "' of graph '" + graph.getName() + "' does not evaluate to a boolean: " + t.condition); 558 } 559 boolean bool = Boolean.TRUE.equals(res); 560 t.setResult(bool); 561 if (bool) { 562 trueTrans.add(t); 563 if (executeOnlyFirstTransition()) { 564 // if node is exclusive, no need to evaluate others 565 break; 566 } 567 } 568 saveDocument(); 569 } catch (DocumentRouteException e) { 570 throw e; 571 } catch (RuntimeException e) { 572 throw new DocumentRouteException("Error evaluating condition: " + t.condition, e); 573 } 574 } 575 return trueTrans; 576 } 577 578 @Override 579 public List<String> evaluateTaskAssignees() throws DocumentRouteException { 580 List<String> taskAssignees = new ArrayList<>(); 581 String taskAssigneesVar = getTaskAssigneesVar(); 582 if (StringUtils.isEmpty(taskAssigneesVar)) { 583 return taskAssignees; 584 } 585 try (OperationContext context = getExecutionContext(getSession())) { 586 Expression expr = Scripting.newExpression(taskAssigneesVar); 587 Object res = expr.eval(context); 588 589 if (res instanceof List<?>) { 590 res = ((List<?>) res).toArray(); 591 } 592 if (res instanceof Object[]) { 593 // try to convert to String[] 594 Object[] list = (Object[]) res; 595 String[] tmp = new String[list.length]; 596 try { 597 System.arraycopy(list, 0, tmp, 0, list.length); 598 res = tmp; 599 } catch (ArrayStoreException e) { 600 // one of the elements is not a String 601 } 602 } 603 if (!(res instanceof String || res instanceof String[])) { 604 throw new DocumentRouteException("Can not evaluate task assignees from " + taskAssigneesVar); 605 } 606 if (res instanceof String) { 607 taskAssignees.add((String) res); 608 } else { 609 taskAssignees.addAll(Arrays.asList((String[]) res)); 610 } 611 } catch (DocumentRouteException e) { 612 throw e; 613 } catch (RuntimeException e) { 614 throw new DocumentRouteException("Error evaluating task assignees: " + taskAssigneesVar, e); 615 } 616 return taskAssignees; 617 } 618 619 @Override 620 public boolean canMerge() { 621 int n = 0; 622 List<Transition> inputTransitions = getInputTransitions(); 623 624 for (Transition t : inputTransitions) { 625 if (t.result) { 626 n++; 627 } 628 } 629 String merge = (String) getProperty(PROP_MERGE); 630 if (MERGE_ONE.equals(merge)) { 631 return n > 0; 632 } else if (MERGE_ALL.equals(merge)) { 633 return n == inputTransitions.size(); 634 } else { 635 throw new NuxeoException("Illegal merge mode '" + merge + "' for node " + this); 636 } 637 } 638 639 @Override 640 public List<Transition> getInputTransitions() { 641 return inputTransitions; 642 } 643 644 @Override 645 public void cancelTasks() { 646 CoreSession session = getSession(); 647 List<TaskInfo> tasks = getTasksInfo(); 648 for (TaskInfo task : tasks) { 649 if (!task.isEnded()) { 650 cancelTask(session, task.getTaskDocId()); 651 } 652 } 653 } 654 655 @Override 656 public List<Button> getTaskButtons() { 657 if (taskButtons == null) { 658 taskButtons = computeTaskButtons(); 659 } 660 return taskButtons; 661 } 662 663 protected List<Button> computeTaskButtons() { 664 ListProperty props = (ListProperty) document.getProperty(PROP_TASK_BUTTONS); 665 List<Button> btns = new ArrayList<>(props.size()); 666 for (Property p : props) { 667 btns.add(new Button(this, p)); 668 } 669 Collections.sort(btns); 670 return btns; 671 } 672 673 @Override 674 public void setButton(String status) { 675 document.setPropertyValue(PROP_NODE_BUTTON, status); 676 saveDocument(); 677 } 678 679 @Override 680 public void setLastActor(String actor) { 681 document.setPropertyValue(PROP_NODE_LAST_ACTOR, actor); 682 saveDocument(); 683 } 684 685 protected void addTaskAssignees(List<String> taskAssignees) { 686 List<String> allTasksAssignees = getTaskAssignees(); 687 allTasksAssignees.addAll(taskAssignees); 688 document.setPropertyValue(PROP_TASK_ASSIGNEES, (Serializable) allTasksAssignees); 689 saveDocument(); 690 } 691 692 @Override 693 public String getTaskDocType() { 694 String taskDocType = (String) getProperty(PROP_TASK_DOC_TYPE); 695 if (StringUtils.isEmpty(taskDocType) || TaskConstants.TASK_TYPE_NAME.equals(taskDocType)) { 696 taskDocType = DocumentRoutingConstants.ROUTING_TASK_DOC_TYPE; 697 } 698 return taskDocType; 699 } 700 701 protected Date evaluateDueDate() throws DocumentRouteException { 702 String taskDueDateExpr = getTaskDueDateExpr(); 703 if (StringUtils.isEmpty(taskDueDateExpr)) { 704 return new Date(); 705 } 706 try (OperationContext context = getExecutionContext(getSession())) { 707 Expression expr = Scripting.newExpression(taskDueDateExpr); 708 Object res = expr.eval(context); 709 if (res instanceof DateWrapper) { 710 return ((DateWrapper) res).getDate(); 711 } else if (res instanceof Date) { 712 return (Date) res; 713 } else if (res instanceof Calendar) { 714 return ((Calendar) res).getTime(); 715 } else if (res instanceof String) { 716 return DateParser.parseW3CDateTime((String) res); 717 } else { 718 throw new DocumentRouteException( 719 "The following expression can not be evaluated to a date: " + taskDueDateExpr); 720 } 721 } catch (DocumentRouteException e) { 722 throw e; 723 } catch (RuntimeException e) { 724 throw new DocumentRouteException("Error evaluating task due date: " + taskDueDateExpr, e); 725 } 726 } 727 728 @Override 729 public Date computeTaskDueDate() throws DocumentRouteException { 730 Date dueDate = evaluateDueDate(); 731 document.setPropertyValue(PROP_TASK_DUE_DATE, dueDate); 732 CoreSession session = document.getCoreSession(); 733 session.saveDocument(document); 734 return dueDate; 735 } 736 737 @Override 738 public boolean executeOnlyFirstTransition() { 739 return getBoolean(PROP_EXECUTE_ONLY_FIRST_TRANSITION); 740 } 741 742 @Override 743 public boolean hasSubRoute() throws DocumentRouteException { 744 return getSubRouteModelId() != null; 745 } 746 747 @Override 748 public String getSubRouteModelId() throws DocumentRouteException { 749 String subRouteModelExpr = (String) getProperty(PROP_SUB_ROUTE_MODEL_EXPR); 750 if (StringUtils.isBlank(subRouteModelExpr)) { 751 return null; 752 } 753 try (OperationContext context = getExecutionContext(getSession())) { 754 String res = valueOrExpression(String.class, subRouteModelExpr, context, "Sub-workflow id expression"); 755 return StringUtils.defaultIfBlank(res, null); 756 } 757 } 758 759 protected String getSubRouteInstanceId() { 760 return (String) getProperty(GraphNode.PROP_SUB_ROUTE_INSTANCE_ID); 761 } 762 763 @Override 764 public DocumentRoute startSubRoute() throws DocumentRouteException { 765 String subRouteModelId = getSubRouteModelId(); 766 // create the instance without starting it 767 DocumentRoutingService service = Framework.getService(DocumentRoutingService.class); 768 List<String> docs = graph.getAttachedDocuments(); 769 String subRouteInstanceId = service.createNewInstance(subRouteModelId, docs, getSession(), false); 770 // set info about parent in subroute 771 DocumentModel subRouteInstance = getSession().getDocument(new IdRef(subRouteInstanceId)); 772 subRouteInstance.setPropertyValue(GraphRoute.PROP_PARENT_ROUTE, getDocument().getParentRef().toString()); 773 subRouteInstance.setPropertyValue(GraphRoute.PROP_PARENT_NODE, getDocument().getName()); 774 subRouteInstance = getSession().saveDocument(subRouteInstance); 775 // set info about subroute in parent 776 document.setPropertyValue(PROP_SUB_ROUTE_INSTANCE_ID, subRouteInstanceId); 777 saveDocument(); 778 // start the sub-route 779 Map<String, Serializable> map = getSubRouteInitialVariables(); 780 service.startInstance(subRouteInstanceId, docs, map, getSession()); 781 // return the sub-route 782 // subRouteInstance.refresh(); 783 DocumentRoute subRoute = subRouteInstance.getAdapter(DocumentRoute.class); 784 return subRoute; 785 } 786 787 protected Map<String, Serializable> getSubRouteInitialVariables() { 788 ListProperty props = (ListProperty) document.getProperty(PROP_SUB_ROUTE_VARS); 789 Map<String, Serializable> map = new HashMap<>(); 790 try (OperationContext context = getExecutionContext(getSession())) { 791 for (Property p : props) { 792 MapProperty prop = (MapProperty) p; 793 String key = (String) prop.get(PROP_KEYVALUE_KEY).getValue(); 794 String v = (String) prop.get(PROP_KEYVALUE_VALUE).getValue(); 795 Serializable value = valueOrExpression(Serializable.class, v, context, 796 "Sub-workflow variable expression"); 797 map.put(key, value); 798 } 799 } 800 return map; 801 } 802 803 /* 804 * Code similar to the one in OperationChainContribution. 805 */ 806 protected <T> T valueOrExpression(Class<T> klass, String v, OperationContext context, String kind) 807 throws DocumentRouteException { 808 if (!v.startsWith(EXPR_PREFIX)) { 809 return (T) v; 810 } 811 v = v.substring(EXPR_PREFIX.length()).trim(); 812 Expression expr; 813 if (v.contains(TEMPLATE_START)) { 814 expr = Scripting.newTemplate(v); 815 } else { 816 expr = Scripting.newExpression(v); 817 } 818 Object res = null; 819 try { 820 res = expr.eval(context); 821 // stupid eval() method throws generic Exception 822 } catch (RuntimeException e) { 823 throw new DocumentRouteException("Error evaluating expression: " + v, e); 824 } 825 if (!(klass.isAssignableFrom(res.getClass()))) { 826 throw new DocumentRouteException( 827 kind + " of node '" + getId() + "' of graph '" + graph.getName() + "' does not evaluate to " 828 + klass.getSimpleName() + " but " + res.getClass().getName() + ": " + v); 829 } 830 return (T) res; 831 } 832 833 @Override 834 public void cancelSubRoute() throws DocumentRouteException { 835 String subRouteInstanceId = getSubRouteInstanceId(); 836 if (!StringUtils.isEmpty(subRouteInstanceId)) { 837 DocumentModel subRouteDoc = getSession().getDocument(new IdRef(subRouteInstanceId)); 838 DocumentRoute subRoute = subRouteDoc.getAdapter(DocumentRoute.class); 839 subRoute.cancel(getSession()); 840 } 841 } 842 843 protected List<EscalationRule> computeEscalationRules() { 844 ListProperty props = (ListProperty) document.getProperty(PROP_ESCALATION_RULES); 845 List<EscalationRule> rules = new ArrayList<>(props.size()); 846 for (Property p : props) { 847 rules.add(new EscalationRule(this, p)); 848 } 849 Collections.sort(rules); 850 return rules; 851 } 852 853 @Override 854 public List<EscalationRule> getEscalationRules() { 855 if (escalationRules == null) { 856 escalationRules = computeEscalationRules(); 857 } 858 return escalationRules; 859 } 860 861 @Override 862 public List<EscalationRule> evaluateEscalationRules() { 863 List<EscalationRule> rulesToExecute = new ArrayList<>(); 864 // add specific helpers for escalation 865 for (EscalationRule rule : getEscalationRules()) { 866 try (OperationContext context = getExecutionContext(getSession())) { 867 Expression expr = new RoutingScriptingExpression(rule.condition, 868 new RoutingScriptingFunctions(context, rule)); 869 Object res = expr.eval(context); 870 if (!(res instanceof Boolean)) { 871 throw new DocumentRouteException("Condition for rule " + rule + " of node '" + getId() 872 + "' of graph '" + graph.getName() + "' does not evaluate to a boolean: " + rule.condition); 873 } 874 boolean bool = Boolean.TRUE.equals(res); 875 if ((!rule.isExecuted() || rule.isMultipleExecution()) && bool) { 876 rulesToExecute.add(rule); 877 } 878 } catch (DocumentRouteException e) { 879 throw e; 880 } catch (RuntimeException e) { 881 throw new DocumentRouteException("Error evaluating condition: " + rule.condition, e); 882 } 883 } 884 saveDocument(); 885 return rulesToExecute; 886 } 887 888 @Override 889 public boolean hasMultipleTasks() { 890 return getBoolean(PROP_HAS_MULTIPLE_TASKS); 891 } 892 893 protected List<TaskInfo> computeTasksInfo() { 894 ListProperty props = (ListProperty) document.getProperty(PROP_TASKS_INFO); 895 List<TaskInfo> tasks = new ArrayList<>(props.size()); 896 for (Property p : props) { 897 tasks.add(new TaskInfo(this, p)); 898 } 899 return tasks; 900 } 901 902 @Override 903 public List<TaskInfo> getTasksInfo() { 904 if (tasksInfo == null) { 905 tasksInfo = computeTasksInfo(); 906 } 907 return tasksInfo; 908 } 909 910 @Override 911 public void addTaskInfo(String taskId) { 912 getTasksInfo().add(new TaskInfo(this, taskId)); 913 saveDocument(); 914 } 915 916 @Override 917 public void removeTaskInfo(String taskId) { 918 ListProperty props = (ListProperty) document.getProperty(PROP_TASKS_INFO); 919 Property propertytoBeRemoved = null; 920 for (Property p : props) { 921 if (taskId.equals(p.get(PROP_TASK_INFO_TASK_DOC_ID).getValue())) { 922 propertytoBeRemoved = p; 923 break; 924 } 925 } 926 if (propertytoBeRemoved != null) { 927 props.remove(propertytoBeRemoved); 928 saveDocument(); 929 tasksInfo = null; 930 } 931 } 932 933 @Override 934 public void updateTaskInfo(String taskId, boolean ended, String status, String actor, String comment) { 935 boolean updated = false; 936 List<TaskInfo> tasksInfo = getTasksInfo(); 937 for (TaskInfo taskInfo : tasksInfo) { 938 if (taskId.equals(taskInfo.getTaskDocId())) { 939 taskInfo.setComment(comment); 940 taskInfo.setStatus(status); 941 taskInfo.setActor(actor); 942 taskInfo.setEnded(true); 943 updated = true; 944 } 945 } 946 // handle backward compatibility 947 if (!updated) { 948 // task created before 5.7.3; 949 TaskInfo ti = new TaskInfo(this, taskId); 950 ti.setActor(actor); 951 ti.setStatus(status); 952 ti.setComment(comment); 953 ti.setEnded(true); 954 getTasksInfo().add(ti); 955 } 956 saveDocument(); 957 } 958 959 @Override 960 public List<TaskInfo> getEndedTasksInfo() { 961 List<TaskInfo> tasksInfo = getTasksInfo(); 962 List<TaskInfo> endedTasks = new ArrayList<>(); 963 for (TaskInfo taskInfo : tasksInfo) { 964 if (taskInfo.isEnded()) { 965 endedTasks.add(taskInfo); 966 } 967 } 968 return endedTasks; 969 } 970 971 @Override 972 public boolean hasOpenTasks() { 973 return getTasksInfo().size() != getEndedTasksInfo().size(); 974 } 975 976 @Override 977 public List<TaskInfo> getProcessedTasksInfo() { 978 List<TaskInfo> tasksInfo = getTasksInfo(); 979 List<TaskInfo> processedTasks = new ArrayList<>(); 980 for (TaskInfo taskInfo : tasksInfo) { 981 if (taskInfo.isEnded() && taskInfo.getStatus() != null) { 982 processedTasks.add(taskInfo); 983 } 984 } 985 return processedTasks; 986 } 987 988 @Override 989 public boolean allowTaskReassignment() { 990 return getBoolean(PROP_ALLOW_TASK_REASSIGNMENT); 991 992 } 993 994 protected void cancelTask(CoreSession session, final String taskId) throws DocumentRouteException { 995 DocumentRef taskRef = new IdRef(taskId); 996 if (!session.exists(taskRef)) { 997 log.info(String.format("Task with id %s does not exist anymore", taskId)); 998 DocumentModelList docs = graph.getAttachedDocumentModels(); 999 Framework.getService(DocumentRoutingService.class).removePermissionsForTaskActors(session, docs, taskId); 1000 NuxeoPrincipal principal = (NuxeoPrincipal) session.getPrincipal(); 1001 String actor = principal.getActingUser(); 1002 updateTaskInfo(taskId, true, null, actor, null); 1003 return; 1004 } 1005 DocumentModel taskDoc = session.getDocument(new IdRef(taskId)); 1006 Task task = taskDoc.getAdapter(Task.class); 1007 if (task == null) { 1008 throw new DocumentRouteException("Invalid taskId: " + taskId); 1009 } 1010 DocumentModelList docs = graph.getAttachedDocumentModels(); 1011 Framework.getService(DocumentRoutingService.class).removePermissionsForTaskActors(session, docs, task); 1012 if (task.isOpened()) { 1013 task.cancel(session); 1014 } 1015 session.saveDocument(task.getDocument()); 1016 // task is considered processed with the status "null" when is 1017 // canceled 1018 // actor 1019 NuxeoPrincipal principal = (NuxeoPrincipal) session.getPrincipal(); 1020 String actor = principal.getActingUser(); 1021 updateTaskInfo(taskId, true, null, actor, null); 1022 } 1023 1024 @Override 1025 public void setVariable(String name, String value) { 1026 Map<String, Serializable> nodeVariables = getVariables(); 1027 if (nodeVariables.containsKey(name)) { 1028 nodeVariables.put(name, value); 1029 setVariables(nodeVariables); 1030 } 1031 } 1032 1033 /** 1034 * @since 7.2 1035 */ 1036 @Override 1037 public boolean hasTaskButton(String name) { 1038 for (Button button : getTaskButtons()) { 1039 if (button.getName().equals(name)) { 1040 return true; 1041 } 1042 } 1043 return false; 1044 } 1045 1046}