View Javadoc

1   /**
2    * Copyright 2005-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.engine.simulation;
17  
18  import org.apache.log4j.MDC;
19  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
20  import org.kuali.rice.kew.actionitem.ActionItem;
21  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
22  import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
23  import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
24  import org.kuali.rice.kew.actionrequest.Recipient;
25  import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
26  import org.kuali.rice.kew.actiontaken.ActionTakenValue;
27  import org.kuali.rice.kew.api.WorkflowRuntimeException;
28  import org.kuali.rice.kew.api.exception.DocumentSimulatedRouteException;
29  import org.kuali.rice.kew.doctype.bo.DocumentType;
30  import org.kuali.rice.kew.engine.ActivationContext;
31  import org.kuali.rice.kew.engine.EngineState;
32  import org.kuali.rice.kew.engine.OrchestrationConfig;
33  import org.kuali.rice.kew.engine.ProcessContext;
34  import org.kuali.rice.kew.engine.RouteContext;
35  import org.kuali.rice.kew.engine.StandardWorkflowEngine;
36  import org.kuali.rice.kew.engine.node.Branch;
37  import org.kuali.rice.kew.engine.node.NoOpNode;
38  import org.kuali.rice.kew.engine.node.NodeJotter;
39  import org.kuali.rice.kew.engine.node.NodeType;
40  import org.kuali.rice.kew.engine.node.ProcessDefinitionBo;
41  import org.kuali.rice.kew.engine.node.RequestsNode;
42  import org.kuali.rice.kew.engine.node.RouteNode;
43  import org.kuali.rice.kew.engine.node.RouteNodeInstance;
44  import org.kuali.rice.kew.engine.node.SimpleNode;
45  import org.kuali.rice.kew.engine.node.service.RouteNodeService;
46  import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
47  import org.kuali.rice.kew.api.exception.ResourceUnavailableException;
48  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
49  import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
50  import org.kuali.rice.kew.service.KEWServiceLocator;
51  import org.kuali.rice.kew.api.KewApiConstants;
52  import org.kuali.rice.kew.util.PerformanceLogger;
53  import org.kuali.rice.kew.util.Utilities;
54  import org.kuali.rice.kim.api.group.Group;
55  import org.kuali.rice.kim.api.identity.Person;
56  
57  import java.io.ByteArrayInputStream;
58  import java.io.ByteArrayOutputStream;
59  import java.io.IOException;
60  import java.io.ObjectInputStream;
61  import java.io.ObjectOutputStream;
62  import java.io.Serializable;
63  import java.sql.Timestamp;
64  import java.util.ArrayList;
65  import java.util.Collections;
66  import java.util.HashSet;
67  import java.util.Iterator;
68  import java.util.List;
69  import java.util.Set;
70  
71  
72  /**
73   * A WorkflowEngine implementation which runs simulations.  This object is not thread-safe
74   * and therefore a new instance needs to be instantiated on every use.
75   *
76   * @author Kuali Rice Team (rice.collab@kuali.org)
77   */
78  public class SimulationEngine extends StandardWorkflowEngine implements SimulationWorkflowEngine {
79  
80      public SimulationEngine () {
81          super();
82      }
83      public SimulationEngine(RouteNodeService routeNodeService, RouteHeaderService routeHeaderService,
84              ParameterService parameterService, OrchestrationConfig config) {
85          super(routeNodeService, routeHeaderService, parameterService, config);
86      }
87  
88      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SimulationEngine.class);
89  
90  	private SimulationCriteria criteria;
91      private SimulationResults results;
92  
93      @Override
94      public SimulationResults runSimulation(SimulationCriteria criteria) throws Exception {
95          try {
96              this.criteria = criteria;
97              this.results = new SimulationResults();
98              validateCriteria(criteria);
99              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.setSupressPolicyErrors(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             isInPath = isInPath || isNodeNameInPath(nodeName, subNode, inspected);
232         }
233         for (Iterator<RouteNode> iterator = node.getNextNodes().iterator(); iterator.hasNext();) {
234             RouteNode nextNode = (RouteNode) iterator.next();
235             isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, inspected);
236         }
237         return isInPath;
238     }
239 
240     private boolean hasReachedCompletion(ProcessContext processContext, List actionRequests, RouteNodeInstance nodeInstance, SimulationCriteria criteria) {
241         if (!criteria.getDestinationRecipients().isEmpty()) {
242             for (Iterator iterator = actionRequests.iterator(); iterator.hasNext();) {
243                 ActionRequestValue request = (ActionRequestValue) iterator.next();
244                 for (Iterator<Recipient> userIt = criteria.getDestinationRecipients().iterator(); userIt.hasNext();) {
245                     Recipient recipient = (Recipient) userIt.next();
246                     if (request.isRecipientRoutedRequest(recipient)) {
247                         if ( (org.apache.commons.lang.StringUtils.isEmpty(criteria.getDestinationNodeName())) || (criteria.getDestinationNodeName().equals(request.getNodeInstance().getName())) ) {
248                             return true;
249                         }
250                     }
251                 }
252             }
253         }
254         return (org.apache.commons.lang.StringUtils.isEmpty(criteria.getDestinationNodeName()) && processContext.isComplete() && processContext.getNextNodeInstances().isEmpty())
255             || nodeInstance.getRouteNode().getRouteNodeName().equals(criteria.getDestinationNodeName());
256     }
257 
258     private List<ActionTakenValue> processPotentialActionsTaken(RouteContext routeContext, DocumentRouteHeaderValue routeHeader, RouteNodeInstance justProcessedNode, SimulationCriteria criteria) {
259     	List<ActionTakenValue> actionsTaken = new ArrayList<ActionTakenValue>();
260     	List requestsToCheck = new ArrayList();
261     	requestsToCheck.addAll(routeContext.getEngineState().getGeneratedRequests());
262         requestsToCheck.addAll(routeHeader.getActionRequests());
263     	List<ActionRequestValue> pendingActionRequestValues = getCriteriaActionsToDoByNodeName(requestsToCheck, justProcessedNode.getName());
264         List<ActionTakenValue> actionsToTakeForNode = generateActionsToTakeForNode(justProcessedNode.getName(), routeHeader, criteria, pendingActionRequestValues);
265 
266         for (ActionTakenValue actionTaken : actionsToTakeForNode)
267         {
268             KEWServiceLocator.getActionRequestService().deactivateRequests(actionTaken, pendingActionRequestValues, routeContext.getActivationContext());
269             actionsTaken.add(actionTaken);
270 //            routeContext.getActivationContext().getSimulatedActionsTaken().add(actionTaken);
271         }
272     	return actionsTaken;
273     }
274 
275     private List<ActionTakenValue> generateActionsToTakeForNode(String nodeName, DocumentRouteHeaderValue routeHeader, SimulationCriteria criteria, List<ActionRequestValue> pendingActionRequests) {
276         List<ActionTakenValue> actions = new ArrayList<ActionTakenValue>();
277         if ( (criteria.getActionsToTake() != null) && (!criteria.getActionsToTake().isEmpty()) ) {
278             for (SimulationActionToTake simAction : criteria.getActionsToTake()) {
279                 if (nodeName.equals(simAction.getNodeName())) {
280                     actions.add(createDummyActionTaken(routeHeader, simAction.getUser(), simAction.getActionToPerform(), findDelegatorForActionRequests(pendingActionRequests)));
281                 }
282             }
283         }
284         return actions;
285     }
286 
287     private List<ActionRequestValue> getCriteriaActionsToDoByNodeName(List generatedRequests, String nodeName) {
288     	List<ActionRequestValue> requests = new ArrayList<ActionRequestValue>();
289         for (Iterator iterator = generatedRequests.iterator(); iterator.hasNext();) {
290             ActionRequestValue request = (ActionRequestValue) iterator.next();
291             if ( (request.isPending()) && request.getNodeInstance() != null && nodeName.equals(request.getNodeInstance().getName())) {
292             	requests.add(request);
293             }
294         }
295         return requests;
296     }
297 
298     private void validateCriteria(SimulationCriteria criteria) {
299     	if (criteria.getDocumentId() == null && org.apache.commons.lang.StringUtils.isEmpty(criteria.getDocumentTypeName())) {
300 		throw new IllegalArgumentException("No document type name or document id given, cannot simulate a document without a document type name or a document id.");
301     	}
302     	if (criteria.getXmlContent() == null) {
303     		criteria.setXmlContent("");
304     	}
305     }
306 
307     /**
308      * Creates the document to run the simulation against by loading it from the database or creating a fake document for
309      * simulation purposes depending on the passed simulation criteria.
310      *
311      * If the documentId is available, we load the document from the database, otherwise we create one based on the given
312      * DocumentType and xml content.
313      */
314     private DocumentRouteHeaderValue createSimulationDocument(String documentId, SimulationCriteria criteria, RouteContext context) {
315     	DocumentRouteHeaderValue document = null;
316     	if (criteria.isDocumentSimulation()) {
317             document = getDocumentForSimulation(documentId);
318             if (!org.apache.commons.lang.StringUtils.isEmpty(criteria.getXmlContent())) {
319                 document.setDocContent(criteria.getXmlContent());
320             }
321     	} else if (criteria.isDocumentTypeSimulation()) {
322         	DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByName(criteria.getDocumentTypeName());
323         	if (documentType == null) {
324         		throw new IllegalArgumentException("Specified document type could not be found for name '"+criteria.getDocumentTypeName()+"'");
325         	}
326         	documentId = context.getEngineState().getNextSimulationId().toString();
327         	document = new DocumentRouteHeaderValue();
328         	context.setDocument(document);
329         	document.setDocumentId(documentId);
330         	document.setCreateDate(new Timestamp(System.currentTimeMillis()));
331         	document.setDocContent(criteria.getXmlContent());
332         	document.setDocRouteLevel(new Integer(0));
333         	document.setDocumentTypeId(documentType.getDocumentTypeId());
334     		document.setDocRouteStatus(KewApiConstants.ROUTE_HEADER_INITIATED_CD);
335     		initializeDocument(document);
336         }
337         if (document == null) {
338         	throw new IllegalArgumentException("Workflow simulation engine could not locate document with id "+documentId);
339         }
340         for (ActionRequestValue actionRequest : document.getActionRequests()) {
341         	actionRequest = (ActionRequestValue) deepCopy(actionRequest);
342         	document.getSimulatedActionRequests().add(actionRequest);
343         	for (ActionItem actionItem : actionRequest.getActionItems()) {
344         		actionRequest.getSimulatedActionItems().add((ActionItem) deepCopy(actionItem));
345         	}
346         }
347         context.setDocument(document);
348         installSimulationNodeInstances(context, criteria);
349 		return document;
350     }
351 
352     private DocumentRouteHeaderValue getDocumentForSimulation(String documentId) {
353         DocumentRouteHeaderValue document = getRouteHeaderService().getRouteHeader(documentId);
354         return (DocumentRouteHeaderValue)deepCopy(document);
355     }
356 
357     private Serializable deepCopy(Serializable src) {
358         Serializable obj = null;
359         if (src != null) {
360             ObjectOutputStream oos = null;
361             ObjectInputStream ois = null;
362             try {
363                 ByteArrayOutputStream serializer = new ByteArrayOutputStream();
364                 oos = new ObjectOutputStream(serializer);
365                 oos.writeObject(src);
366 
367                 ByteArrayInputStream deserializer = new ByteArrayInputStream(serializer.toByteArray());
368                 ois = new ObjectInputStream(deserializer);
369                 obj = (Serializable) ois.readObject();
370             }
371             catch (IOException e) {
372                 throw new RuntimeException("unable to complete deepCopy from src '" + src.toString() + "'", e);
373             }
374             catch (ClassNotFoundException e) {
375                 throw new RuntimeException("unable to complete deepCopy from src '" + src.toString() + "'", e);
376             }
377             finally {
378                 try {
379                     if (oos != null) {
380                         oos.close();
381                     }
382                     if (ois != null) {
383                         ois.close();
384                     }
385                 }
386                 catch (IOException e) {
387                     // ignoring this IOException, since the streams are going to be abandoned now anyway
388                 }
389             }
390         }
391         return obj;
392     }
393 
394     private void routeDocumentIfNecessary(DocumentRouteHeaderValue document, SimulationCriteria criteria, RouteContext routeContext) throws InvalidActionTakenException {
395     	if (criteria.getRoutingUser() != null) {
396             ActionTakenValue action = createDummyActionTaken(document, criteria.getRoutingUser(), KewApiConstants.ACTION_TAKEN_ROUTED_CD, null);
397     		routeContext.getActivationContext().getSimulatedActionsTaken().add(action);
398             simulateDocumentRoute(action, document, criteria.getRoutingUser(), routeContext);
399     	}
400     }
401 
402     /**
403      * Looks at the rule templates and/or the startNodeName and creates the appropriate node instances to run simulation against.
404      * After creating the node instances, it hooks them all together and installs a "terminal" simulation node to stop the simulation
405      * node at the end of the simulation.
406      */
407     private void installSimulationNodeInstances(RouteContext context, SimulationCriteria criteria) {
408     	DocumentRouteHeaderValue document = context.getDocument();
409     	List<RouteNode> simulationNodes = new ArrayList<RouteNode>();
410     	if (!criteria.getNodeNames().isEmpty()) {
411     		for (String nodeName : criteria.getNodeNames()) {
412 				if ( LOG.isDebugEnabled() ) {
413 				    LOG.debug("Installing simulation starting node '"+nodeName+"'");
414 				}
415 	    		List<RouteNode> nodes = KEWServiceLocator.getRouteNodeService().getFlattenedNodes(document.getDocumentType(), true);
416 	    		boolean foundNode = false;
417 	    		for (RouteNode node : nodes) {
418 					if (node.getRouteNodeName().equals(nodeName)) {
419 						simulationNodes.add(node);
420 						foundNode = true;
421 						break;
422 					}
423 				}
424 	    		if (!foundNode) {
425 	    			throw new IllegalArgumentException("Could not find node on the document type for the given name '"+nodeName+"'");
426 	    		}
427     		}
428     	} else if (!criteria.getRuleTemplateNames().isEmpty()) {
429     		List<RouteNode> nodes = KEWServiceLocator.getRouteNodeService().getFlattenedNodes(document.getDocumentType(), true);
430     		for (String ruleTemplateName : criteria.getRuleTemplateNames()) {
431 				boolean foundNode = false;
432 				for (RouteNode node : nodes) {
433 					String routeMethodName = node.getRouteMethodName();
434 					if (node.isFlexRM() && ruleTemplateName.equals(routeMethodName)) {
435 						simulationNodes.add(node);
436 						foundNode = true;
437 						break;
438 					}
439 				}
440 				if (!foundNode) {
441 	    			throw new IllegalArgumentException("Could not find node on the document type with the given rule template name '"+ruleTemplateName+"'");
442 	    		}
443 			}
444     	} else if (criteria.isFlattenNodes()) {
445     		// if they want to flatten the nodes, we will essentially process all simple nodes that are defined on the DocumentType
446             List<RouteNode> nodes = KEWServiceLocator.getRouteNodeService().getFlattenedNodes(document.getDocumentType(), true);
447             for ( RouteNode node : nodes ) {
448                 try {
449 	                if ( NodeType.fromNode( node ).isTypeOf( SimpleNode.class ) 
450 	                		&& !NodeType.fromNode( node ).isTypeOf( NoOpNode.class ) ) {
451 	                    simulationNodes.add(node);
452 	                }
453                 } catch (ResourceUnavailableException ex) {
454 					LOG.warn( "Unable to determine node type in simulator: " + ex.getMessage() );
455 				}
456             }
457     	} else {
458     	    // in this case, we want to let the document proceed from it's current active node
459     		return;
460     	}
461     	
462     	// hook all of the simulation nodes together
463     	Branch defaultBranch = document.getInitialRouteNodeInstances().get(0).getBranch();
464     	// clear out the initial route node instances, we are going to build a new node path based on what we want to simulate
465     	document.getInitialRouteNodeInstances().clear();
466 
467     	RouteNodeInstance currentNodeInstance = null;//initialNodeInstance;
468     	for (RouteNode simulationNode : simulationNodes) {
469 			RouteNodeInstance nodeInstance = helper.getNodeFactory().createRouteNodeInstance(document.getDocumentId(), simulationNode);
470 			nodeInstance.setBranch(defaultBranch);
471 			if (currentNodeInstance == null) {
472 				document.getInitialRouteNodeInstances().add(nodeInstance);
473 				nodeInstance.setActive(true);
474 				saveNode(context, nodeInstance);
475 			} else {
476 				currentNodeInstance.addNextNodeInstance(nodeInstance);
477 				saveNode(context, currentNodeInstance);
478 			}
479 			currentNodeInstance = nodeInstance;
480 		}
481     	installSimulationTerminationNode(context, document.getDocumentType(), currentNodeInstance);
482     }
483 
484     private void installSimulationTerminationNode(RouteContext context, DocumentType documentType, RouteNodeInstance lastNodeInstance) {
485     	RouteNode terminationNode = new RouteNode();
486     	terminationNode.setDocumentType(documentType);
487     	terminationNode.setDocumentTypeId(documentType.getDocumentTypeId());
488     	terminationNode.setNodeType(NoOpNode.class.getName());
489     	terminationNode.setRouteNodeName("SIMULATION_TERMINATION_NODE");
490     	RouteNodeInstance terminationNodeInstance = helper.getNodeFactory().createRouteNodeInstance(lastNodeInstance.getDocumentId(), terminationNode);
491     	terminationNodeInstance.setBranch(lastNodeInstance.getBranch());
492     	lastNodeInstance.addNextNodeInstance(terminationNodeInstance);
493     	saveNode(context, lastNodeInstance);
494     }
495 
496     // below is pretty much a copy of RouteDocumentAction... but actions have to be faked for now
497     private void simulateDocumentRoute(ActionTakenValue actionTaken, DocumentRouteHeaderValue document, Person user, RouteContext routeContext) throws InvalidActionTakenException {
498         if (document.isRouted()) {
499             throw new WorkflowRuntimeException("Document can not simulate a route if it has already been routed");
500         }
501     	ActionRequestService actionRequestService = KEWServiceLocator.getActionRequestService();
502         // TODO delyea - deep copy below
503         List<ActionRequestValue> actionRequests = new ArrayList<ActionRequestValue>();
504         for (Iterator iter = actionRequestService.findPendingByDoc(document.getDocumentId()).iterator(); iter.hasNext();) {
505             ActionRequestValue arv = (ActionRequestValue) deepCopy( (ActionRequestValue) iter.next() );
506             for (ActionItem actionItem : arv.getActionItems()) {
507         		arv.getSimulatedActionItems().add((ActionItem) deepCopy(actionItem));
508         	}
509             actionRequests.add(arv);//(ActionRequestValue)deepCopy(arv));
510         }
511 //        actionRequests.addAll(actionRequestService.findPendingByDoc(document.getDocumentId()));
512         LOG.debug("Simulate Deactivating all pending action requests");
513         // deactivate any requests for the user that routed the document.
514         for (Iterator<ActionRequestValue> iter = actionRequests.iterator(); iter.hasNext();) {
515             ActionRequestValue actionRequest = (ActionRequestValue) iter.next();
516             // requests generated to the user who is routing the document should be deactivated
517             if ( (user.getPrincipalId().equals(actionRequest.getPrincipalId())) && (actionRequest.isActive()) ) {
518             	actionRequestService.deactivateRequest(actionTaken, actionRequest, routeContext.getActivationContext());
519             }
520             // requests generated by a save action should be deactivated
521             else if (KewApiConstants.SAVED_REQUEST_RESPONSIBILITY_ID.equals(actionRequest.getResponsibilityId())) {
522             	actionRequestService.deactivateRequest(actionTaken, actionRequest, routeContext.getActivationContext());
523             }
524         }
525 
526 //        String oldStatus = document.getDocRouteStatus();
527         document.markDocumentEnroute();
528 //        String newStatus = document.getDocRouteStatus();
529 //        notifyStatusChange(newStatus, oldStatus);
530 //        getRouteHeaderService().saveRouteHeader(document);
531     }
532 
533     private ActionTakenValue createDummyActionTaken(DocumentRouteHeaderValue routeHeader, Person userToPerformAction, String actionToPerform, Recipient delegator) {
534         ActionTakenValue val = new ActionTakenValue();
535         val.setActionTaken(actionToPerform);
536         if (KewApiConstants.ACTION_TAKEN_ROUTED_CD.equals(actionToPerform)) {
537             val.setActionTaken(KewApiConstants.ACTION_TAKEN_COMPLETED_CD);
538         }
539 		val.setAnnotation("");
540 		val.setDocVersion(routeHeader.getDocVersion());
541 		val.setDocumentId(routeHeader.getDocumentId());
542 		val.setPrincipalId(userToPerformAction.getPrincipalId());
543 
544 		if (delegator != null) {
545 			if (delegator instanceof KimPrincipalRecipient) {
546 				val.setDelegatorPrincipalId(((KimPrincipalRecipient) delegator).getPrincipalId());
547 			} else if (delegator instanceof KimGroupRecipient) {
548 				Group group = ((KimGroupRecipient) delegator).getGroup();
549 				val.setDelegatorGroupId(group.getId());
550 			} else{
551 				throw new IllegalArgumentException("Invalid Recipient type received: " + delegator.getClass().getName());
552 			}
553 		}
554 
555 		//val.setRouteHeader(routeHeader);
556 		val.setCurrentIndicator(Boolean.TRUE);
557 		return val;
558     }
559 
560 	/**
561 	 * Used by actions taken
562 	 *
563 	 * Returns the highest priority delegator in the list of action requests.
564 	 */
565 	private Recipient findDelegatorForActionRequests(List<ActionRequestValue> actionRequests) {
566 		return KEWServiceLocator.getActionRequestService().findDelegator(actionRequests);
567 	}
568 
569     /**
570      * Executes a "saveNode" for the simulation engine, this does not actually save the document, but rather
571      * assigns it some simulation ids.
572      *
573      * Resolves KULRICE-368
574      */
575     @Override
576     protected void saveNode(RouteContext context, RouteNodeInstance nodeInstance) {
577 		// we shold be in simulation mode here
578 
579     	if (nodeInstance.getRouteNodeInstanceId() == null) {
580     		nodeInstance.setRouteNodeInstanceId(context.getEngineState().getNextSimulationId());
581     	}
582     	
583     	// if we are in simulation mode, lets go ahead and assign some id
584     	// values to our beans
585     	for (Iterator<RouteNodeInstance> iterator = nodeInstance.getNextNodeInstances().iterator(); iterator.hasNext();) {
586     		RouteNodeInstance routeNodeInstance = (RouteNodeInstance) iterator.next();
587     		if (routeNodeInstance.getRouteNodeInstanceId() == null) {
588     			routeNodeInstance.setRouteNodeInstanceId(context.getEngineState().getNextSimulationId());
589     		}
590     	}
591     	if (nodeInstance.getProcess() != null && nodeInstance.getProcess().getRouteNodeInstanceId() == null) {
592     		nodeInstance.getProcess().setRouteNodeInstanceId(context.getEngineState().getNextSimulationId());
593     	}
594     	if (nodeInstance.getBranch() != null && nodeInstance.getBranch().getBranchId() == null) {
595     		nodeInstance.getBranch().setBranchId(context.getEngineState().getNextSimulationId());
596     	}
597     }
598 
599 }