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