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