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