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