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