View Javadoc

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