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}