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