001    /**
002     * Copyright 2005-2013 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     */
016    package org.kuali.rice.kew.engine.simulation;
017    
018    import org.apache.log4j.MDC;
019    import org.kuali.rice.coreservice.framework.parameter.ParameterService;
020    import org.kuali.rice.kew.actionitem.ActionItem;
021    import org.kuali.rice.kew.actionrequest.ActionRequestValue;
022    import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
023    import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
024    import org.kuali.rice.kew.actionrequest.Recipient;
025    import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
026    import org.kuali.rice.kew.actiontaken.ActionTakenValue;
027    import org.kuali.rice.kew.api.WorkflowRuntimeException;
028    import org.kuali.rice.kew.api.exception.DocumentSimulatedRouteException;
029    import org.kuali.rice.kew.doctype.bo.DocumentType;
030    import org.kuali.rice.kew.engine.ActivationContext;
031    import org.kuali.rice.kew.engine.EngineState;
032    import org.kuali.rice.kew.engine.OrchestrationConfig;
033    import org.kuali.rice.kew.engine.ProcessContext;
034    import org.kuali.rice.kew.engine.RouteContext;
035    import org.kuali.rice.kew.engine.StandardWorkflowEngine;
036    import org.kuali.rice.kew.engine.node.Branch;
037    import org.kuali.rice.kew.engine.node.NoOpNode;
038    import org.kuali.rice.kew.engine.node.NodeJotter;
039    import org.kuali.rice.kew.engine.node.NodeType;
040    import org.kuali.rice.kew.engine.node.ProcessDefinitionBo;
041    import org.kuali.rice.kew.engine.node.RequestsNode;
042    import org.kuali.rice.kew.engine.node.RouteNode;
043    import org.kuali.rice.kew.engine.node.RouteNodeInstance;
044    import org.kuali.rice.kew.engine.node.SimpleNode;
045    import org.kuali.rice.kew.engine.node.service.RouteNodeService;
046    import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
047    import org.kuali.rice.kew.api.exception.ResourceUnavailableException;
048    import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
049    import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
050    import org.kuali.rice.kew.service.KEWServiceLocator;
051    import org.kuali.rice.kew.api.KewApiConstants;
052    import org.kuali.rice.kew.util.PerformanceLogger;
053    import org.kuali.rice.kew.util.Utilities;
054    import org.kuali.rice.kim.api.group.Group;
055    import org.kuali.rice.kim.api.identity.Person;
056    
057    import java.io.ByteArrayInputStream;
058    import java.io.ByteArrayOutputStream;
059    import java.io.IOException;
060    import java.io.ObjectInputStream;
061    import java.io.ObjectOutputStream;
062    import java.io.Serializable;
063    import java.sql.Timestamp;
064    import java.util.ArrayList;
065    import java.util.Collections;
066    import java.util.HashSet;
067    import java.util.Iterator;
068    import java.util.List;
069    import java.util.Set;
070    
071    
072    /**
073     * A WorkflowEngine implementation which runs simulations.  This object is not thread-safe
074     * and therefore a new instance needs to be instantiated on every use.
075     *
076     * @author Kuali Rice Team (rice.collab@kuali.org)
077     */
078    public class SimulationEngine extends StandardWorkflowEngine implements SimulationWorkflowEngine {
079    
080        public SimulationEngine () {
081            super();
082        }
083        public SimulationEngine(RouteNodeService routeNodeService, RouteHeaderService routeHeaderService,
084                ParameterService parameterService, OrchestrationConfig config) {
085            super(routeNodeService, routeHeaderService, parameterService, config);
086        }
087    
088        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SimulationEngine.class);
089    
090            private SimulationCriteria criteria;
091        private SimulationResults results;
092    
093        @Override
094        public SimulationResults runSimulation(SimulationCriteria criteria) throws Exception {
095            try {
096                this.criteria = criteria;
097                this.results = new SimulationResults();
098                validateCriteria(criteria);
099                process(criteria.getDocumentId(), null);
100                return results;
101            } finally {
102                //nulling out the results & criteria since these really should only be local variables.
103                this.criteria = null;
104                this.results = null;
105            }
106        }
107    
108        @Override
109        public void process(String documentId, String nodeInstanceId) throws InvalidActionTakenException, DocumentSimulatedRouteException {
110            RouteContext context = RouteContext.createNewRouteContext();
111            try {
112                    ActivationContext activationContext = new ActivationContext(ActivationContext.CONTEXT_IS_SIMULATION);
113                    if (criteria.isActivateRequests() == null) {
114                        activationContext.setActivateRequests(!criteria.getActionsToTake().isEmpty());
115                    } else {
116                        activationContext.setActivateRequests(criteria.isActivateRequests().booleanValue());
117                    }
118                    context.setActivationContext(activationContext);
119                    context.setEngineState(new EngineState());
120                    // suppress policy errors when running a simulation for the purposes of display on the route log
121                    RequestsNode.setSuppressPolicyErrors(context);
122                    DocumentRouteHeaderValue document = createSimulationDocument(documentId, criteria, context);
123                document.setInitiatorWorkflowId("simulation");
124                    if ( (criteria.isDocumentSimulation()) && ( (document.isProcessed()) || (document.isFinal()) ) ) {
125                            results.setDocument(document);
126                            return;
127                    }
128                    routeDocumentIfNecessary(document, criteria, context);
129                    results.setDocument(document);
130                    documentId = document.getDocumentId();
131                    
132                    // detect if MDC already has docId param (to avoid nuking it below)
133                    boolean mdcHadDocId = MDC.get("docId") != null;
134                    if (!mdcHadDocId) { MDC.put("docId", documentId); }
135                    
136                    PerformanceLogger perfLog = new PerformanceLogger(documentId);
137                    try {
138                        if ( LOG.isInfoEnabled() ) {
139                            LOG.info("Processing document for Simulation: " + documentId);
140                        }
141                            List<RouteNodeInstance> activeNodeInstances = getRouteNodeService().getActiveNodeInstances(document);
142                            List<RouteNodeInstance> nodeInstancesToProcess = determineNodeInstancesToProcess(activeNodeInstances, criteria.getDestinationNodeName());
143    
144                            context.setDocument(document);
145                            // TODO set document content
146                            context.setEngineState(new EngineState());
147                            ProcessContext processContext = new ProcessContext(true, nodeInstancesToProcess);
148                            while (! nodeInstancesToProcess.isEmpty()) {
149                                    RouteNodeInstance nodeInstance = (RouteNodeInstance)nodeInstancesToProcess.remove(0);
150                                    if ( !nodeInstance.isActive() ) {
151                                            continue;
152                                    }
153                                    NodeJotter.jotNodeInstance(context.getDocument(), nodeInstance);
154                                    context.setNodeInstance(nodeInstance);
155                                    processContext = processNodeInstance(context, helper);
156                                    if (!hasReachedCompletion(processContext, context.getEngineState().getGeneratedRequests(), nodeInstance, criteria)) {
157                                            if (processContext.isComplete()) {
158                                                    if (!processContext.getNextNodeInstances().isEmpty()) {
159                                                            nodeInstancesToProcess.addAll(processContext.getNextNodeInstances());
160                                                    }
161                                                    context.getActivationContext().getSimulatedActionsTaken().addAll(processPotentialActionsTaken(context, document, nodeInstance, criteria));
162                                            }
163                                    } else {
164                                            context.getActivationContext().getSimulatedActionsTaken().addAll(processPotentialActionsTaken(context, document, nodeInstance, criteria));
165                                    }
166                            }
167                            List simulatedActionRequests = context.getEngineState().getGeneratedRequests();
168                            Collections.sort(simulatedActionRequests, new Utilities.RouteLogActionRequestSorter());
169                            results.setSimulatedActionRequests(simulatedActionRequests);
170                            results.setSimulatedActionsTaken(context.getActivationContext().getSimulatedActionsTaken());
171                } catch (InvalidActionTakenException e) {
172                    throw e;
173                } catch (Exception e) {
174                    String errorMsg = "Error running simulation for document " + ((criteria.isDocumentSimulation()) ? "id " + documentId.toString() : "type " + criteria.getDocumentTypeName());
175                    LOG.error(errorMsg,e);
176                    throw new DocumentSimulatedRouteException(errorMsg, e);
177                    } finally {
178                            perfLog.log("Time to run simulation.");
179                            RouteContext.clearCurrentRouteContext();
180                            
181                            if (!mdcHadDocId) { MDC.remove("docID"); }
182                    }
183            } finally {
184                    RouteContext.releaseCurrentRouteContext();
185            }
186        }
187    
188        /**
189         * If there are multiple paths, we need to figure out which ones we need to follow for blanket approval.
190         * This method will throw an exception if a node with the given name could not be located in the routing path.
191         * This method is written in such a way that it should be impossible for there to be an infinate loop, even if
192         * there is extensive looping in the node graph.
193         */
194        private List<RouteNodeInstance> determineNodeInstancesToProcess(List<RouteNodeInstance> activeNodeInstances, String nodeName) throws InvalidActionTakenException {
195            if (org.apache.commons.lang.StringUtils.isEmpty(nodeName)) {
196                return activeNodeInstances;
197            }
198            List<RouteNodeInstance> nodeInstancesToProcess = new ArrayList<RouteNodeInstance>();
199            for (RouteNodeInstance nodeInstance : activeNodeInstances) {
200                if (nodeName.equals(nodeInstance.getName())) {
201                    // one of active node instances is node instance to stop at
202                    return new ArrayList<RouteNodeInstance>();
203                } else {
204                    if (isNodeNameInPath(nodeName, nodeInstance)) {
205                        nodeInstancesToProcess.add(nodeInstance);
206                    }
207                }
208            }
209            if (nodeInstancesToProcess.size() == 0) {
210                throw new InvalidActionTakenException("Could not locate a node with the given name in the blanket approval path '" + nodeName + "'.  " +
211                        "The document is probably already passed the specified node or does not contain the node.");
212            }
213            return nodeInstancesToProcess;
214        }
215    
216        private boolean isNodeNameInPath(String nodeName, RouteNodeInstance nodeInstance) {
217            boolean isInPath = false;
218            for (Iterator<RouteNode> iterator = nodeInstance.getRouteNode().getNextNodes().iterator(); iterator.hasNext();) {
219                RouteNode nextNode = (RouteNode) iterator.next();
220                isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, new HashSet<String>());
221            }
222            return isInPath;
223        }
224    
225        private boolean isNodeNameInPath(String nodeName, RouteNode node, Set<String> inspected) {
226            boolean isInPath = !inspected.contains(node.getRouteNodeId()) && node.getRouteNodeName().equals(nodeName);
227            inspected.add(node.getRouteNodeId());
228            if (helper.isSubProcessNode(node)) {
229                ProcessDefinitionBo subProcess = node.getDocumentType().getNamedProcess(node.getRouteNodeName());
230                RouteNode subNode = subProcess.getInitialRouteNode();
231                if (subNode != null) {
232                    isInPath = isInPath || isNodeNameInPath(nodeName, subNode, inspected);
233                }
234            }
235            for (Iterator<RouteNode> iterator = node.getNextNodes().iterator(); iterator.hasNext();) {
236                RouteNode nextNode = (RouteNode) iterator.next();
237                isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, inspected);
238            }
239            return isInPath;
240        }
241    
242        private boolean hasReachedCompletion(ProcessContext processContext, List actionRequests, RouteNodeInstance nodeInstance, SimulationCriteria criteria) {
243            if (!criteria.getDestinationRecipients().isEmpty()) {
244                for (Iterator iterator = actionRequests.iterator(); iterator.hasNext();) {
245                    ActionRequestValue request = (ActionRequestValue) iterator.next();
246                    for (Iterator<Recipient> userIt = criteria.getDestinationRecipients().iterator(); userIt.hasNext();) {
247                        Recipient recipient = (Recipient) userIt.next();
248                        if (request.isRecipientRoutedRequest(recipient)) {
249                            if ( (org.apache.commons.lang.StringUtils.isEmpty(criteria.getDestinationNodeName())) || (criteria.getDestinationNodeName().equals(request.getNodeInstance().getName())) ) {
250                                return true;
251                            }
252                        }
253                    }
254                }
255            }
256            return (org.apache.commons.lang.StringUtils.isEmpty(criteria.getDestinationNodeName()) && processContext.isComplete() && processContext.getNextNodeInstances().isEmpty())
257                || nodeInstance.getRouteNode().getRouteNodeName().equals(criteria.getDestinationNodeName());
258        }
259    
260        private List<ActionTakenValue> processPotentialActionsTaken(RouteContext routeContext, DocumentRouteHeaderValue routeHeader, RouteNodeInstance justProcessedNode, SimulationCriteria criteria) {
261            List<ActionTakenValue> actionsTaken = new ArrayList<ActionTakenValue>();
262            List requestsToCheck = new ArrayList();
263            requestsToCheck.addAll(routeContext.getEngineState().getGeneratedRequests());
264            requestsToCheck.addAll(routeHeader.getActionRequests());
265            List<ActionRequestValue> pendingActionRequestValues = getCriteriaActionsToDoByNodeName(requestsToCheck, justProcessedNode.getName());
266            List<ActionTakenValue> actionsToTakeForNode = generateActionsToTakeForNode(justProcessedNode.getName(), routeHeader, criteria, pendingActionRequestValues);
267    
268            for (ActionTakenValue actionTaken : actionsToTakeForNode)
269            {
270                KEWServiceLocator.getActionRequestService().deactivateRequests(actionTaken, pendingActionRequestValues, routeContext.getActivationContext());
271                actionsTaken.add(actionTaken);
272    //            routeContext.getActivationContext().getSimulatedActionsTaken().add(actionTaken);
273            }
274            return actionsTaken;
275        }
276    
277        private List<ActionTakenValue> generateActionsToTakeForNode(String nodeName, DocumentRouteHeaderValue routeHeader, SimulationCriteria criteria, List<ActionRequestValue> pendingActionRequests) {
278            List<ActionTakenValue> actions = new ArrayList<ActionTakenValue>();
279            if ( (criteria.getActionsToTake() != null) && (!criteria.getActionsToTake().isEmpty()) ) {
280                for (SimulationActionToTake simAction : criteria.getActionsToTake()) {
281                    if (nodeName.equals(simAction.getNodeName())) {
282                        actions.add(createDummyActionTaken(routeHeader, simAction.getUser(), simAction.getActionToPerform(), findDelegatorForActionRequests(pendingActionRequests)));
283                    }
284                }
285            }
286            return actions;
287        }
288    
289        private List<ActionRequestValue> getCriteriaActionsToDoByNodeName(List generatedRequests, String nodeName) {
290            List<ActionRequestValue> requests = new ArrayList<ActionRequestValue>();
291            for (Iterator iterator = generatedRequests.iterator(); iterator.hasNext();) {
292                ActionRequestValue request = (ActionRequestValue) iterator.next();
293                if ( (request.isPending()) && request.getNodeInstance() != null && nodeName.equals(request.getNodeInstance().getName())) {
294                    requests.add(request);
295                }
296            }
297            return requests;
298        }
299    
300        private void validateCriteria(SimulationCriteria criteria) {
301            if (criteria.getDocumentId() == null && org.apache.commons.lang.StringUtils.isEmpty(criteria.getDocumentTypeName())) {
302                    throw new IllegalArgumentException("No document type name or document id given, cannot simulate a document without a document type name or a document id.");
303            }
304            if (criteria.getXmlContent() == null) {
305                    criteria.setXmlContent("");
306            }
307        }
308    
309        /**
310         * Creates the document to run the simulation against by loading it from the database or creating a fake document for
311         * simulation purposes depending on the passed simulation criteria.
312         *
313         * If the documentId is available, we load the document from the database, otherwise we create one based on the given
314         * DocumentType and xml content.
315         */
316        private DocumentRouteHeaderValue createSimulationDocument(String documentId, SimulationCriteria criteria, RouteContext context) {
317            DocumentRouteHeaderValue document = null;
318            if (criteria.isDocumentSimulation()) {
319                document = getDocumentForSimulation(documentId);
320                if (!org.apache.commons.lang.StringUtils.isEmpty(criteria.getXmlContent())) {
321                    document.setDocContent(criteria.getXmlContent());
322                }
323            } else if (criteria.isDocumentTypeSimulation()) {
324                    DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByName(criteria.getDocumentTypeName());
325                    if (documentType == null) {
326                            throw new IllegalArgumentException("Specified document type could not be found for name '"+criteria.getDocumentTypeName()+"'");
327                    }
328                    documentId = context.getEngineState().getNextSimulationId().toString();
329                    document = new DocumentRouteHeaderValue();
330                    context.setDocument(document);
331                    document.setDocumentId(documentId);
332                    document.setCreateDate(new Timestamp(System.currentTimeMillis()));
333                    document.setDocContent(criteria.getXmlContent());
334                    document.setDocRouteLevel(new Integer(0));
335                    document.setDocumentTypeId(documentType.getDocumentTypeId());
336                    document.setDocRouteStatus(KewApiConstants.ROUTE_HEADER_INITIATED_CD);
337                    initializeDocument(document);
338            }
339            if (document == null) {
340                    throw new IllegalArgumentException("Workflow simulation engine could not locate document with id "+documentId);
341            }
342            for (ActionRequestValue actionRequest : document.getActionRequests()) {
343                    actionRequest = (ActionRequestValue) deepCopy(actionRequest);
344                    document.getSimulatedActionRequests().add(actionRequest);
345                    for (ActionItem actionItem : actionRequest.getActionItems()) {
346                            actionRequest.getSimulatedActionItems().add((ActionItem) deepCopy(actionItem));
347                    }
348            }
349            context.setDocument(document);
350            installSimulationNodeInstances(context, criteria);
351                    return document;
352        }
353    
354        private DocumentRouteHeaderValue getDocumentForSimulation(String documentId) {
355            DocumentRouteHeaderValue document = getRouteHeaderService().getRouteHeader(documentId);
356            return (DocumentRouteHeaderValue)deepCopy(document);
357        }
358    
359        private Serializable deepCopy(Serializable src) {
360            Serializable obj = null;
361            if (src != null) {
362                ObjectOutputStream oos = null;
363                ObjectInputStream ois = null;
364                try {
365                    ByteArrayOutputStream serializer = new ByteArrayOutputStream();
366                    oos = new ObjectOutputStream(serializer);
367                    oos.writeObject(src);
368    
369                    ByteArrayInputStream deserializer = new ByteArrayInputStream(serializer.toByteArray());
370                    ois = new ObjectInputStream(deserializer);
371                    obj = (Serializable) ois.readObject();
372                }
373                catch (IOException e) {
374                    throw new RuntimeException("unable to complete deepCopy from src '" + src.toString() + "'", e);
375                }
376                catch (ClassNotFoundException e) {
377                    throw new RuntimeException("unable to complete deepCopy from src '" + src.toString() + "'", e);
378                }
379                finally {
380                    try {
381                        if (oos != null) {
382                            oos.close();
383                        }
384                        if (ois != null) {
385                            ois.close();
386                        }
387                    }
388                    catch (IOException e) {
389                        // ignoring this IOException, since the streams are going to be abandoned now anyway
390                    }
391                }
392            }
393            return obj;
394        }
395    
396        private void routeDocumentIfNecessary(DocumentRouteHeaderValue document, SimulationCriteria criteria, RouteContext routeContext) throws InvalidActionTakenException {
397            if (criteria.getRoutingUser() != null) {
398                ActionTakenValue action = createDummyActionTaken(document, criteria.getRoutingUser(), KewApiConstants.ACTION_TAKEN_ROUTED_CD, null);
399                    routeContext.getActivationContext().getSimulatedActionsTaken().add(action);
400                simulateDocumentRoute(action, document, criteria.getRoutingUser(), routeContext);
401            }
402        }
403    
404        /**
405         * Looks at the rule templates and/or the startNodeName and creates the appropriate node instances to run simulation against.
406         * After creating the node instances, it hooks them all together and installs a "terminal" simulation node to stop the simulation
407         * node at the end of the simulation.
408         */
409        private void installSimulationNodeInstances(RouteContext context, SimulationCriteria criteria) {
410            DocumentRouteHeaderValue document = context.getDocument();
411            List<RouteNode> simulationNodes = new ArrayList<RouteNode>();
412            if (!criteria.getNodeNames().isEmpty()) {
413                    for (String nodeName : criteria.getNodeNames()) {
414                                    if ( LOG.isDebugEnabled() ) {
415                                        LOG.debug("Installing simulation starting node '"+nodeName+"'");
416                                    }
417                            List<RouteNode> nodes = KEWServiceLocator.getRouteNodeService().getFlattenedNodes(document.getDocumentType(), true);
418                            boolean foundNode = false;
419                            for (RouteNode node : nodes) {
420                                            if (node.getRouteNodeName().equals(nodeName)) {
421                                                    simulationNodes.add(node);
422                                                    foundNode = true;
423                                                    break;
424                                            }
425                                    }
426                            if (!foundNode) {
427                                    throw new IllegalArgumentException("Could not find node on the document type for the given name '"+nodeName+"'");
428                            }
429                    }
430            } else if (!criteria.getRuleTemplateNames().isEmpty()) {
431                    List<RouteNode> nodes = KEWServiceLocator.getRouteNodeService().getFlattenedNodes(document.getDocumentType(), true);
432                    for (String ruleTemplateName : criteria.getRuleTemplateNames()) {
433                                    boolean foundNode = false;
434                                    for (RouteNode node : nodes) {
435                                            String routeMethodName = node.getRouteMethodName();
436                                            if (node.isFlexRM() && ruleTemplateName.equals(routeMethodName)) {
437                                                    simulationNodes.add(node);
438                                                    foundNode = true;
439                                                    break;
440                                            }
441                                    }
442                                    if (!foundNode) {
443                                    throw new IllegalArgumentException("Could not find node on the document type with the given rule template name '"+ruleTemplateName+"'");
444                            }
445                            }
446            } else if (criteria.isFlattenNodes()) {
447                    // if they want to flatten the nodes, we will essentially process all simple nodes that are defined on the DocumentType
448                List<RouteNode> nodes = KEWServiceLocator.getRouteNodeService().getFlattenedNodes(document.getDocumentType(), true);
449                for ( RouteNode node : nodes ) {
450                    try {
451                            if ( NodeType.fromNode( node ).isTypeOf( SimpleNode.class ) 
452                                            && !NodeType.fromNode( node ).isTypeOf( NoOpNode.class ) ) {
453                                simulationNodes.add(node);
454                            }
455                    } catch (ResourceUnavailableException ex) {
456                                            LOG.warn( "Unable to determine node type in simulator: " + ex.getMessage() );
457                                    }
458                }
459            } else {
460                // in this case, we want to let the document proceed from it's current active node
461                    return;
462            }
463            
464            // hook all of the simulation nodes together
465            Branch defaultBranch = document.getInitialRouteNodeInstances().get(0).getBranch();
466            // clear out the initial route node instances, we are going to build a new node path based on what we want to simulate
467            document.getInitialRouteNodeInstances().clear();
468    
469            RouteNodeInstance currentNodeInstance = null;//initialNodeInstance;
470            for (RouteNode simulationNode : simulationNodes) {
471                            RouteNodeInstance nodeInstance = helper.getNodeFactory().createRouteNodeInstance(document.getDocumentId(), simulationNode);
472                            nodeInstance.setBranch(defaultBranch);
473                            if (currentNodeInstance == null) {
474                                    document.getInitialRouteNodeInstances().add(nodeInstance);
475                                    nodeInstance.setActive(true);
476                                    saveNode(context, nodeInstance);
477                            } else {
478                                    currentNodeInstance.addNextNodeInstance(nodeInstance);
479                                    saveNode(context, currentNodeInstance);
480                            }
481                            currentNodeInstance = nodeInstance;
482                    }
483            installSimulationTerminationNode(context, document.getDocumentType(), currentNodeInstance);
484        }
485    
486        private void installSimulationTerminationNode(RouteContext context, DocumentType documentType, RouteNodeInstance lastNodeInstance) {
487            RouteNode terminationNode = new RouteNode();
488            terminationNode.setDocumentType(documentType);
489            terminationNode.setDocumentTypeId(documentType.getDocumentTypeId());
490            terminationNode.setNodeType(NoOpNode.class.getName());
491            terminationNode.setRouteNodeName("SIMULATION_TERMINATION_NODE");
492            RouteNodeInstance terminationNodeInstance = helper.getNodeFactory().createRouteNodeInstance(lastNodeInstance.getDocumentId(), terminationNode);
493            terminationNodeInstance.setBranch(lastNodeInstance.getBranch());
494            lastNodeInstance.addNextNodeInstance(terminationNodeInstance);
495            saveNode(context, lastNodeInstance);
496        }
497    
498        // below is pretty much a copy of RouteDocumentAction... but actions have to be faked for now
499        private void simulateDocumentRoute(ActionTakenValue actionTaken, DocumentRouteHeaderValue document, Person user, RouteContext routeContext) throws InvalidActionTakenException {
500            if (document.isRouted()) {
501                throw new WorkflowRuntimeException("Document can not simulate a route if it has already been routed");
502            }
503            ActionRequestService actionRequestService = KEWServiceLocator.getActionRequestService();
504            // TODO delyea - deep copy below
505            List<ActionRequestValue> actionRequests = new ArrayList<ActionRequestValue>();
506            for (Iterator iter = actionRequestService.findPendingByDoc(document.getDocumentId()).iterator(); iter.hasNext();) {
507                ActionRequestValue arv = (ActionRequestValue) deepCopy( (ActionRequestValue) iter.next() );
508                for (ActionItem actionItem : arv.getActionItems()) {
509                            arv.getSimulatedActionItems().add((ActionItem) deepCopy(actionItem));
510                    }
511                actionRequests.add(arv);//(ActionRequestValue)deepCopy(arv));
512            }
513    //        actionRequests.addAll(actionRequestService.findPendingByDoc(document.getDocumentId()));
514            LOG.debug("Simulate Deactivating all pending action requests");
515            // deactivate any requests for the user that routed the document.
516            for (Iterator<ActionRequestValue> iter = actionRequests.iterator(); iter.hasNext();) {
517                ActionRequestValue actionRequest = (ActionRequestValue) iter.next();
518                // requests generated to the user who is routing the document should be deactivated
519                if ( (user.getPrincipalId().equals(actionRequest.getPrincipalId())) && (actionRequest.isActive()) ) {
520                    actionRequestService.deactivateRequest(actionTaken, actionRequest, routeContext.getActivationContext());
521                }
522                // requests generated by a save action should be deactivated
523                else if (KewApiConstants.SAVED_REQUEST_RESPONSIBILITY_ID.equals(actionRequest.getResponsibilityId())) {
524                    actionRequestService.deactivateRequest(actionTaken, actionRequest, routeContext.getActivationContext());
525                }
526            }
527    
528    //        String oldStatus = document.getDocRouteStatus();
529            document.markDocumentEnroute();
530    //        String newStatus = document.getDocRouteStatus();
531    //        notifyStatusChange(newStatus, oldStatus);
532    //        getRouteHeaderService().saveRouteHeader(document);
533        }
534    
535        private ActionTakenValue createDummyActionTaken(DocumentRouteHeaderValue routeHeader, Person userToPerformAction, String actionToPerform, Recipient delegator) {
536            ActionTakenValue val = new ActionTakenValue();
537            val.setActionTaken(actionToPerform);
538            if (KewApiConstants.ACTION_TAKEN_ROUTED_CD.equals(actionToPerform)) {
539                val.setActionTaken(KewApiConstants.ACTION_TAKEN_COMPLETED_CD);
540            }
541                    val.setAnnotation("");
542                    val.setDocVersion(routeHeader.getDocVersion());
543                    val.setDocumentId(routeHeader.getDocumentId());
544                    val.setPrincipalId(userToPerformAction.getPrincipalId());
545    
546                    if (delegator != null) {
547                            if (delegator instanceof KimPrincipalRecipient) {
548                                    val.setDelegatorPrincipalId(((KimPrincipalRecipient) delegator).getPrincipalId());
549                            } else if (delegator instanceof KimGroupRecipient) {
550                                    Group group = ((KimGroupRecipient) delegator).getGroup();
551                                    val.setDelegatorGroupId(group.getId());
552                            } else{
553                                    throw new IllegalArgumentException("Invalid Recipient type received: " + delegator.getClass().getName());
554                            }
555                    }
556    
557                    //val.setRouteHeader(routeHeader);
558                    val.setCurrentIndicator(Boolean.TRUE);
559                    return val;
560        }
561    
562            /**
563             * Used by actions taken
564             *
565             * Returns the highest priority delegator in the list of action requests.
566             */
567            private Recipient findDelegatorForActionRequests(List<ActionRequestValue> actionRequests) {
568                    return KEWServiceLocator.getActionRequestService().findDelegator(actionRequests);
569            }
570    
571        /**
572         * Executes a "saveNode" for the simulation engine, this does not actually save the document, but rather
573         * assigns it some simulation ids.
574         *
575         * Resolves KULRICE-368
576         */
577        @Override
578        protected void saveNode(RouteContext context, RouteNodeInstance nodeInstance) {
579                    // we shold be in simulation mode here
580    
581            if (nodeInstance.getRouteNodeInstanceId() == null) {
582                    nodeInstance.setRouteNodeInstanceId(context.getEngineState().getNextSimulationId());
583            }
584            
585            // if we are in simulation mode, lets go ahead and assign some id
586            // values to our beans
587            for (Iterator<RouteNodeInstance> iterator = nodeInstance.getNextNodeInstances().iterator(); iterator.hasNext();) {
588                    RouteNodeInstance routeNodeInstance = (RouteNodeInstance) iterator.next();
589                    if (routeNodeInstance.getRouteNodeInstanceId() == null) {
590                            routeNodeInstance.setRouteNodeInstanceId(context.getEngineState().getNextSimulationId());
591                    }
592            }
593            if (nodeInstance.getProcess() != null && nodeInstance.getProcess().getRouteNodeInstanceId() == null) {
594                    nodeInstance.getProcess().setRouteNodeInstanceId(context.getEngineState().getNextSimulationId());
595            }
596            if (nodeInstance.getBranch() != null && nodeInstance.getBranch().getBranchId() == null) {
597                    nodeInstance.getBranch().setBranchId(context.getEngineState().getNextSimulationId());
598            }
599        }
600    
601    }