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}