001/** 002 * Copyright 2005-2016 The Kuali Foundation 003 * 004 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php 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 */ 016package org.kuali.rice.kew.actions; 017 018import org.apache.log4j.Logger; 019import org.apache.log4j.MDC; 020import org.kuali.rice.kew.actionrequest.ActionRequestFactory; 021import org.kuali.rice.kew.actionrequest.ActionRequestValue; 022import org.kuali.rice.kew.actionrequest.Recipient; 023import org.kuali.rice.kew.actiontaken.ActionTakenValue; 024import org.kuali.rice.kew.api.WorkflowRuntimeException; 025import org.kuali.rice.kew.api.action.ActionRequestType; 026import org.kuali.rice.kew.api.action.ActionType; 027import org.kuali.rice.kew.api.doctype.DocumentTypePolicy; 028import org.kuali.rice.kew.api.exception.InvalidActionTakenException; 029import org.kuali.rice.kew.doctype.bo.DocumentType; 030import org.kuali.rice.kew.engine.CompatUtils; 031import org.kuali.rice.kew.engine.RouteHelper; 032import org.kuali.rice.kew.engine.node.NodeGraphSearchCriteria; 033import org.kuali.rice.kew.engine.node.NodeGraphSearchResult; 034import org.kuali.rice.kew.engine.node.RouteNode; 035import org.kuali.rice.kew.engine.node.RouteNodeInstance; 036import org.kuali.rice.kew.engine.node.service.RouteNodeService; 037import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange; 038import org.kuali.rice.kew.framework.postprocessor.PostProcessor; 039import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport; 040import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 041import org.kuali.rice.kew.service.KEWServiceLocator; 042import org.kuali.rice.kew.api.KewApiConstants; 043import org.kuali.rice.kim.api.identity.principal.Principal; 044import org.kuali.rice.kim.api.identity.principal.PrincipalContract; 045 046 047import java.util.ArrayList; 048import java.util.Collection; 049import java.util.Iterator; 050import java.util.List; 051 052 053/** 054 * Returns a document to a previous node in the route. 055 * 056 * Current implementation only supports returning to a node on the main branch of the 057 * document. 058 * 059 * @author Kuali Rice Team (rice.collab@kuali.org) 060 */ 061public class ReturnToPreviousNodeAction extends ActionTakenEvent { 062 protected static final Logger LOG = Logger.getLogger(ReturnToPreviousNodeAction.class); 063 064 // ReturnToPrevious returns to initial node when sent a null node name 065 protected static final String INITIAL_NODE_NAME = null; 066 protected static final boolean DEFAULT_SEND_NOTIFICATIONS = true; 067 068 private final RouteHelper helper = new RouteHelper(); 069 protected final String nodeName; 070 private boolean superUserUsage; 071 private final boolean sendNotifications; 072 private final boolean sendNotificationsForPreviousRequests; 073 074 public ReturnToPreviousNodeAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal) { 075 this(KewApiConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS_CD, routeHeader, principal, DEFAULT_ANNOTATION, INITIAL_NODE_NAME, DEFAULT_SEND_NOTIFICATIONS); 076 } 077 078 public ReturnToPreviousNodeAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, String nodeName, boolean sendNotifications) { 079 this(KewApiConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS_CD, routeHeader, principal, annotation, nodeName, sendNotifications); 080 } 081 082 public ReturnToPreviousNodeAction(DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, String nodeName, boolean sendNotifications, boolean runPostProcessorLogic) { 083 this(KewApiConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS_CD, routeHeader, principal, annotation, nodeName, sendNotifications, runPostProcessorLogic); 084 } 085 086 /** 087 * Constructor used to override the action taken code...e.g. when being performed as part of a Move action 088 */ 089 protected ReturnToPreviousNodeAction(String overrideActionTakenCode, DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, String nodeName, boolean sendNotifications) { 090 this(overrideActionTakenCode, routeHeader, principal, annotation, nodeName, sendNotifications, DEFAULT_RUN_POSTPROCESSOR_LOGIC); 091 } 092 093 /** 094 * Constructor used to override the action taken code...e.g. when being performed as part of a Move action 095 */ 096 protected ReturnToPreviousNodeAction(String overrideActionTakenCode, DocumentRouteHeaderValue routeHeader, PrincipalContract principal, String annotation, String nodeName, boolean sendNotifications, boolean runPostProcessorLogic) { 097 super(overrideActionTakenCode, routeHeader, principal, annotation, runPostProcessorLogic); 098 this.nodeName = nodeName; 099 this.sendNotifications = isPolicySet(routeHeader.getDocumentType(), DocumentTypePolicy.NOTIFY_PENDING_ON_RETURN, sendNotifications); 100 this.sendNotificationsForPreviousRequests = isPolicySet(routeHeader.getDocumentType(), DocumentTypePolicy.NOTIFY_COMPLETED_ON_RETURN); 101 } 102 103 /** 104 * Revokes requests, deactivating them with the specified ActionTakenValue. Sends FYI notifications if sendNotifications is true. 105 * TODO will this work properly in the case of an ALL APPROVE role requests with some of the requests already completed? 106 */ 107 private void revokePendingRequests(List<ActionRequestValue> pendingRequests, ActionTakenValue actionTaken, PrincipalContract principal, Recipient delegator) { 108 revokeRequests(pendingRequests); 109 getActionRequestService().deactivateRequests(actionTaken, pendingRequests); 110 if (sendNotifications) { 111 generateNotificationsForRevokedRequests(pendingRequests, principal, delegator); 112 } 113 } 114 115 /** 116 * Revokes requests (not deactivating them). Sends FYI notifications if sendNotifications is true. 117 */ 118 private void revokePreviousRequests(List<ActionRequestValue> actionRequests, PrincipalContract principal, Recipient delegator) { 119 revokeRequests(actionRequests); 120 if (sendNotificationsForPreviousRequests) { 121 generateNotificationsForRevokedRequests(actionRequests, principal, delegator); 122 } 123 } 124 125 /** 126 * Generates FYIs for revoked ActionRequests 127 * @param revokedRequests the revoked actionrequests 128 * @param principal principal taking action, omitted from notifications 129 * @param delegator delegator to omit from notifications 130 */ 131 private void generateNotificationsForRevokedRequests(List<ActionRequestValue> revokedRequests, PrincipalContract principal, Recipient delegator) { 132 ActionRequestFactory arFactory = new ActionRequestFactory(getRouteHeader()); 133 List<ActionRequestValue> notificationRequests = arFactory.generateNotifications(revokedRequests, principal, delegator, KewApiConstants.ACTION_REQUEST_FYI_REQ, getActionTakenCode()); 134 getActionRequestService().activateRequests(notificationRequests); 135 } 136 137 /** 138 * Takes a list of root action requests and marks them and all of their children as "non-current". 139 */ 140 private void revokeRequests(List<ActionRequestValue> actionRequests) { 141 for (Iterator<ActionRequestValue> iterator = actionRequests.iterator(); iterator.hasNext();) { 142 ActionRequestValue actionRequest = iterator.next(); 143 actionRequest.setCurrentIndicator(Boolean.FALSE); 144 if (actionRequest.getActionTaken() != null) { 145 actionRequest.getActionTaken().setCurrentIndicator(Boolean.FALSE); 146 KEWServiceLocator.getActionTakenService().saveActionTaken(actionRequest.getActionTaken()); 147 } 148 revokeRequests(actionRequest.getChildrenRequests()); 149 KEWServiceLocator.getActionRequestService().saveActionRequest(actionRequest); 150 } 151 } 152 153 /** 154 * Template method that determines what action request to generate when returning to initiator 155 * @return the ActionRequestType 156 */ 157 protected ActionRequestType getReturnToInitiatorActionRequestType() { 158 return ActionRequestType.APPROVE; 159 } 160 161 private void processReturnToInitiator(RouteNodeInstance newNodeInstance) { 162 // important to pull this from the RouteNode's DocumentType so we get the proper version 163 RouteNode initialNode = newNodeInstance.getRouteNode().getDocumentType().getPrimaryProcess().getInitialRouteNode(); 164 if (initialNode != null) { 165 if (newNodeInstance.getRouteNode().getRouteNodeId().equals(initialNode.getRouteNodeId())) { 166 LOG.debug("Document was returned to initiator"); 167 ActionRequestFactory arFactory = new ActionRequestFactory(getRouteHeader(), newNodeInstance); 168 ActionRequestValue notificationRequest = arFactory.createNotificationRequest(getReturnToInitiatorActionRequestType().getCode(), determineInitialNodePrincipal(getRouteHeader()), getActionTakenCode(), getPrincipal(), "Document initiator"); 169 getActionRequestService().activateRequest(notificationRequest); 170 } 171 } 172 } 173 174 /** 175 * Determines which principal to generate an actionqrequest when the document is returned to the initial node 176 * By default this is the document initiator. 177 * @param routeHeader the document route header 178 * @return a Principal 179 */ 180 protected PrincipalContract determineInitialNodePrincipal(DocumentRouteHeaderValue routeHeader) { 181 return routeHeader.getInitiatorPrincipal(); 182 } 183 184 /* (non-Javadoc) 185 * @see org.kuali.rice.kew.actions.ActionTakenEvent#isActionCompatibleRequest(java.util.List) 186 */ 187 @Override 188 public String validateActionRules() { 189 return validateActionRules(getActionRequestService().findAllPendingRequests(routeHeader.getDocumentId())); 190 } 191 192 public String validateActionRules(List<ActionRequestValue> actionRequests) { 193 if (!getRouteHeader().isValidActionToTake(getActionPerformedCode())) { 194 String docStatus = getRouteHeader().getDocRouteStatus(); 195 return "Document of status '" + docStatus + "' cannot taken action '" + KewApiConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS + "' to node name "+nodeName; 196 } 197 List<ActionRequestValue> filteredActionRequests = findApplicableActionRequests(actionRequests); 198 if (! isActionCompatibleRequest(filteredActionRequests) && ! isSuperUserUsage()) { 199 return "No request for the user is compatible with the " + ActionType.fromCode(this.getActionTakenCode()).getLabel() + " action"; 200 } 201 return ""; 202 } 203 204 /** 205 * Allows subclasses to determine which actionrequests to inspect for purposes of action validation 206 * @param actionRequests all actionrequests for this document 207 * @return a (possibly) filtered list of actionrequests 208 */ 209 protected List<ActionRequestValue> findApplicableActionRequests(List<ActionRequestValue> actionRequests) { 210 return filterActionRequestsByCode(actionRequests, KewApiConstants.ACTION_REQUEST_COMPLETE_REQ); 211 } 212 213 /* (non-Javadoc) 214 * @see org.kuali.rice.kew.actions.ActionTakenEvent#isActionCompatibleRequest(java.util.List) 215 */ 216 @Override 217 public boolean isActionCompatibleRequest(List<ActionRequestValue> requests) { 218 String actionTakenCode = getActionPerformedCode(); 219 220 // Move is always correct because the client application has authorized it 221 if (KewApiConstants.ACTION_TAKEN_MOVE_CD.equals(actionTakenCode)) { 222 return true; 223 } 224 225 // can always cancel saved or initiated document 226 if (routeHeader.isStateInitiated() || routeHeader.isStateSaved()) { 227 return true; 228 } 229 230 boolean actionCompatible = false; 231 Iterator<ActionRequestValue> ars = requests.iterator(); 232 ActionRequestValue actionRequest = null; 233 234 while (ars.hasNext()) { 235 actionRequest = ars.next(); 236 237 //if (actionRequest.isWorkgroupRequest() && !actionRequest.getWorkgroup().hasMember(this.delegator)) { 238 // TODO might not need this, if so, do role check 239 /*if (actionRequest.isWorkgroupRequest() && !actionRequest.getWorkgroup().hasMember(this.user)) { 240 continue; 241 }*/ 242 243 String request = actionRequest.getActionRequested(); 244 245 if ( (KewApiConstants.ACTION_REQUEST_FYI_REQ.equals(request)) || 246 (KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ.equals(request)) || 247 (KewApiConstants.ACTION_REQUEST_APPROVE_REQ.equals(request)) || 248 (KewApiConstants.ACTION_REQUEST_COMPLETE_REQ.equals(request)) ) { 249 actionCompatible = true; 250 break; 251 } 252 253 // RETURN_TO_PREVIOUS_ROUTE_LEVEL action available only if you've been routed a complete or approve request 254 if (KewApiConstants.ACTION_TAKEN_RETURNED_TO_PREVIOUS_CD.equals(actionTakenCode) && 255 (KewApiConstants.ACTION_REQUEST_COMPLETE_REQ.equals(request) || KewApiConstants.ACTION_REQUEST_APPROVE_REQ.equals(request))) { 256 actionCompatible = true; 257 } 258 } 259 260 return actionCompatible; 261 } 262 263 public void recordAction() throws InvalidActionTakenException { 264 MDC.put("docId", getRouteHeader().getDocumentId()); 265 updateSearchableAttributesIfPossible(); 266 LOG.debug("Returning document " + getRouteHeader().getDocumentId() + " to previous node: " + nodeName + ", annotation: " + annotation); 267 268 List actionRequests = getActionRequestService().findAllValidRequests(getPrincipal().getPrincipalId(), getDocumentId(), KewApiConstants.ACTION_REQUEST_COMPLETE_REQ); 269 String errorMessage = validateActionRules(actionRequests); 270 if (!org.apache.commons.lang.StringUtils.isEmpty(errorMessage)) { 271 throw new InvalidActionTakenException(errorMessage); 272 } 273 274 Collection activeNodeInstances = KEWServiceLocator.getRouteNodeService().getActiveNodeInstances(getRouteHeader().getDocumentId()); 275 NodeGraphSearchCriteria criteria = new NodeGraphSearchCriteria(NodeGraphSearchCriteria.SEARCH_DIRECTION_BACKWARD, activeNodeInstances, nodeName); 276 NodeGraphSearchResult result = KEWServiceLocator.getRouteNodeService().searchNodeGraph(criteria); 277 validateReturnPoint(nodeName, activeNodeInstances, result); 278 279 LOG.debug("Record the returnToPreviousNode action"); 280 // determines the highest priority delegator in the list of action requests 281 // this delegator will be used to save the action taken, and omitted from notification request generation 282 Recipient delegator = findDelegatorForActionRequests(actionRequests); 283 ActionTakenValue actionTaken = saveActionTaken(Boolean.FALSE, delegator); 284 285 LOG.debug("Finding requests in return path and setting current indicator to FALSE"); 286 List<ActionRequestValue> doneRequests = new ArrayList<ActionRequestValue>(); 287 List<ActionRequestValue> pendingRequests = new ArrayList<ActionRequestValue>(); 288 for (RouteNodeInstance nodeInstance : (List<RouteNodeInstance>)result.getPath()) { 289 // mark the node instance as having been revoked 290 KEWServiceLocator.getRouteNodeService().revokeNodeInstance(getRouteHeader(), nodeInstance); 291 List<ActionRequestValue> nodeRequests = getActionRequestService().findRootRequestsByDocIdAtRouteNode(getRouteHeader().getDocumentId(), nodeInstance.getRouteNodeInstanceId()); 292 for (ActionRequestValue request : nodeRequests) { 293 if (request.isDone()) { 294 doneRequests.add(request); 295 } else { 296 pendingRequests.add(request); 297 } 298 } 299 } 300 revokePreviousRequests(doneRequests, getPrincipal(), delegator); 301 LOG.debug("Change pending requests to FYI and activate for docId " + getRouteHeader().getDocumentId()); 302 revokePendingRequests(pendingRequests, actionTaken, getPrincipal(), delegator); 303 notifyActionTaken(actionTaken); 304 executeNodeChange(activeNodeInstances, result); 305 sendAdditionalNotifications(); 306 } 307 308 /** 309 * Template method subclasses can use to send addition notification upon a return to previous action. 310 * This occurs after the postprocessors have been called and the node has been changed 311 */ 312 protected void sendAdditionalNotifications() { 313 // no implementation 314 } 315 316 /** 317 * This method runs various validation checks on the nodes we ended up at so as to make sure we don't 318 * invoke strange return scenarios. 319 */ 320 private void validateReturnPoint(String nodeName, Collection activeNodeInstances, NodeGraphSearchResult result) throws InvalidActionTakenException { 321 RouteNodeInstance resultNodeInstance = result.getResultNodeInstance(); 322 if (result.getResultNodeInstance() == null) { 323 throw new InvalidActionTakenException("Could not locate return point for node name '"+nodeName+"'."); 324 } 325 assertValidNodeType(resultNodeInstance); 326 assertValidBranch(resultNodeInstance, activeNodeInstances); 327 assertValidProcess(resultNodeInstance, activeNodeInstances); 328 assertFinalApprovalNodeNotInPath(result.getPath()); 329 } 330 331 private void assertValidNodeType(RouteNodeInstance resultNodeInstance) throws InvalidActionTakenException { 332 // the return point can only be a simple or a split node 333 if (!helper.isSimpleNode(resultNodeInstance.getRouteNode()) && !helper.isSplitNode(resultNodeInstance.getRouteNode())) { 334 throw new InvalidActionTakenException("Can only return to a simple or a split node, attempting to return to " + resultNodeInstance.getRouteNode().getNodeType()); 335 } 336 } 337 338 private void assertValidBranch(RouteNodeInstance resultNodeInstance, Collection activeNodeInstances) throws InvalidActionTakenException { 339 // the branch of the return point needs to be the same as one of the branches of the active nodes or the same as the root branch 340 boolean inValidBranch = false; 341 if (resultNodeInstance.getBranch().getParentBranch() == null) { 342 inValidBranch = true; 343 } else { 344 for (Iterator iterator = activeNodeInstances.iterator(); iterator.hasNext(); ) { 345 RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator.next(); 346 if (nodeInstance.getBranch().getBranchId().equals(resultNodeInstance.getBranch().getBranchId())) { 347 inValidBranch = true; 348 break; 349 } 350 } 351 } 352 if (!inValidBranch) { 353 throw new InvalidActionTakenException("Returning to an illegal branch, can only return to node within the same branch as an active node or to the primary branch."); 354 } 355 } 356 357 private void assertValidProcess(RouteNodeInstance resultNodeInstance, Collection activeNodeInstances) throws InvalidActionTakenException { 358 // if we are in a process, we need to return within the same process 359 if (resultNodeInstance.isInProcess()) { 360 boolean inValidProcess = false; 361 for (Iterator iterator = activeNodeInstances.iterator(); iterator.hasNext(); ) { 362 RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator.next(); 363 if (nodeInstance.isInProcess() && nodeInstance.getProcess().getRouteNodeInstanceId().equals(nodeInstance.getProcess().getRouteNodeInstanceId())) { 364 inValidProcess = true; 365 break; 366 } 367 } 368 if (!inValidProcess) { 369 throw new InvalidActionTakenException("Returning into an illegal process, cannot return to node within a previously executing process."); 370 } 371 } 372 } 373 374 /** 375 * Cannot return past a COMPLETE final approval node. This means that you can return from an active and incomplete final approval node. 376 * @param path 377 * @throws InvalidActionTakenException 378 */ 379 private void assertFinalApprovalNodeNotInPath(List path) throws InvalidActionTakenException { 380 for (Iterator iterator = path.iterator(); iterator.hasNext(); ) { 381 RouteNodeInstance nodeInstance = (RouteNodeInstance ) iterator.next(); 382 // if we have a complete final approval node in our path, we cannot return past it 383 if (nodeInstance.isComplete() && Boolean.TRUE.equals(nodeInstance.getRouteNode().getFinalApprovalInd())) { 384 throw new InvalidActionTakenException("Cannot return past or through the final approval node '"+nodeInstance.getName()+"'."); 385 } 386 } 387 } 388 389 private void executeNodeChange(Collection activeNodes, NodeGraphSearchResult result) throws InvalidActionTakenException { 390 Integer oldRouteLevel = null; 391 Integer newRouteLevel = null; 392 if (CompatUtils.isRouteLevelCompatible(getRouteHeader())) { 393 int returnPathLength = result.getPath().size()-1; 394 oldRouteLevel = getRouteHeader().getDocRouteLevel(); 395 newRouteLevel = oldRouteLevel - returnPathLength; 396 LOG.debug("Changing route header "+ getRouteHeader().getDocumentId()+" route level for backward compatibility to "+newRouteLevel); 397 getRouteHeader().setDocRouteLevel(newRouteLevel); 398 KEWServiceLocator.getRouteHeaderService().saveRouteHeader(routeHeader); 399 } 400 List<RouteNodeInstance> startingNodes = determineStartingNodes(result.getPath(), activeNodes); 401 RouteNodeInstance newNodeInstance = materializeReturnPoint(startingNodes, result); 402 for (RouteNodeInstance activeNode : startingNodes) 403 { 404 notifyNodeChange(oldRouteLevel, newRouteLevel, activeNode, newNodeInstance); 405 } 406 processReturnToInitiator(newNodeInstance); 407 } 408 409 private void notifyNodeChange(Integer oldRouteLevel, Integer newRouteLevel, RouteNodeInstance oldNodeInstance, RouteNodeInstance newNodeInstance) throws InvalidActionTakenException { 410 try { 411 LOG.debug("Notifying post processor of route node change '"+oldNodeInstance.getName()+"'->'"+newNodeInstance.getName()); 412 PostProcessor postProcessor = routeHeader.getDocumentType().getPostProcessor(); 413 KEWServiceLocator.getRouteHeaderService().saveRouteHeader(getRouteHeader()); 414 DocumentRouteLevelChange routeNodeChange = new DocumentRouteLevelChange(routeHeader.getDocumentId(), 415 routeHeader.getAppDocId(), 416 oldRouteLevel, newRouteLevel, 417 oldNodeInstance.getName(), newNodeInstance.getName(), 418 oldNodeInstance.getRouteNodeInstanceId(), newNodeInstance.getRouteNodeInstanceId()); 419 ProcessDocReport report = postProcessor.doRouteLevelChange(routeNodeChange); 420 setRouteHeader(KEWServiceLocator.getRouteHeaderService().getRouteHeader(getDocumentId())); 421 if (!report.isSuccess()) { 422 LOG.warn(report.getMessage(), report.getProcessException()); 423 throw new InvalidActionTakenException(report.getMessage()); 424 } 425 } catch (Exception ex) { 426 throw new WorkflowRuntimeException(ex.getMessage()); 427 } 428 } 429 430 private List<RouteNodeInstance> determineStartingNodes(List path, Collection<RouteNodeInstance> activeNodes) { 431 List<RouteNodeInstance> startingNodes = new ArrayList<RouteNodeInstance>(); 432 for (RouteNodeInstance activeNodeInstance : activeNodes) 433 { 434 if (isInPath(activeNodeInstance, path)) 435 { 436 startingNodes.add(activeNodeInstance); 437 } 438 } 439 return startingNodes; 440 } 441 442 private boolean isInPath(RouteNodeInstance nodeInstance, List<RouteNodeInstance> path) { 443 for (RouteNodeInstance pathNodeInstance : path) 444 { 445 if (pathNodeInstance.getRouteNodeInstanceId().equals(nodeInstance.getRouteNodeInstanceId())) 446 { 447 return true; 448 } 449 } 450 return false; 451 } 452 453 private RouteNodeInstance materializeReturnPoint(Collection<RouteNodeInstance> startingNodes, NodeGraphSearchResult result) { 454 RouteNodeService nodeService = KEWServiceLocator.getRouteNodeService(); 455 RouteNodeInstance returnInstance = result.getResultNodeInstance(); 456 RouteNodeInstance newNodeInstance = helper.getNodeFactory().createRouteNodeInstance(getDocumentId(), returnInstance.getRouteNode()); 457 newNodeInstance.setBranch(returnInstance.getBranch()); 458 newNodeInstance.setProcess(returnInstance.getProcess()); 459 newNodeInstance.setComplete(false); 460 newNodeInstance.setActive(true); 461 nodeService.save(newNodeInstance); 462 for (RouteNodeInstance activeNodeInstance : startingNodes) { 463 // TODO what if the activeNodeInstance already has next nodes? 464 activeNodeInstance.setComplete(true); 465 activeNodeInstance.setActive(false); 466 activeNodeInstance.setInitial(false); 467 activeNodeInstance.addNextNodeInstance(newNodeInstance); 468 } 469 for (RouteNodeInstance activeNodeInstance : startingNodes) 470 { 471 nodeService.save(activeNodeInstance); 472 } 473 // TODO really we need to call transitionTo on this node, how can we do that? 474 // this isn't an issue yet because we only allow simple nodes and split nodes at the moment which do no real 475 // work on transitionTo but we may need to enhance that in the future 476 return newNodeInstance; 477 } 478 479 public boolean isSuperUserUsage() { 480 return superUserUsage; 481 } 482 public void setSuperUserUsage(boolean superUserUsage) { 483 this.superUserUsage = superUserUsage; 484 } 485 486}