View Javadoc

1   /**
2    * Copyright 2005-2013 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.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 }