001/**
002 * Copyright 2005-2015 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.kew.engine.simulation;
017
018import org.apache.log4j.MDC;
019import org.kuali.rice.coreservice.framework.parameter.ParameterService;
020import org.kuali.rice.kew.actionitem.ActionItem;
021import org.kuali.rice.kew.actionrequest.ActionRequestValue;
022import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
023import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
024import org.kuali.rice.kew.actionrequest.Recipient;
025import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
026import org.kuali.rice.kew.actiontaken.ActionTakenValue;
027import org.kuali.rice.kew.api.WorkflowRuntimeException;
028import org.kuali.rice.kew.api.exception.DocumentSimulatedRouteException;
029import org.kuali.rice.kew.doctype.bo.DocumentType;
030import org.kuali.rice.kew.engine.ActivationContext;
031import org.kuali.rice.kew.engine.EngineState;
032import org.kuali.rice.kew.engine.OrchestrationConfig;
033import org.kuali.rice.kew.engine.ProcessContext;
034import org.kuali.rice.kew.engine.RouteContext;
035import org.kuali.rice.kew.engine.StandardWorkflowEngine;
036import org.kuali.rice.kew.engine.node.Branch;
037import org.kuali.rice.kew.engine.node.NoOpNode;
038import org.kuali.rice.kew.engine.node.NodeJotter;
039import org.kuali.rice.kew.engine.node.NodeType;
040import org.kuali.rice.kew.engine.node.ProcessDefinitionBo;
041import org.kuali.rice.kew.engine.node.RequestsNode;
042import org.kuali.rice.kew.engine.node.RouteNode;
043import org.kuali.rice.kew.engine.node.RouteNodeInstance;
044import org.kuali.rice.kew.engine.node.SimpleNode;
045import org.kuali.rice.kew.engine.node.service.RouteNodeService;
046import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
047import org.kuali.rice.kew.api.exception.ResourceUnavailableException;
048import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
049import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
050import org.kuali.rice.kew.service.KEWServiceLocator;
051import org.kuali.rice.kew.api.KewApiConstants;
052import org.kuali.rice.kew.util.PerformanceLogger;
053import org.kuali.rice.kew.util.Utilities;
054import org.kuali.rice.kim.api.group.Group;
055import org.kuali.rice.kim.api.identity.Person;
056
057import java.io.ByteArrayInputStream;
058import java.io.ByteArrayOutputStream;
059import java.io.IOException;
060import java.io.ObjectInputStream;
061import java.io.ObjectOutputStream;
062import java.io.Serializable;
063import java.sql.Timestamp;
064import java.util.ArrayList;
065import java.util.Collections;
066import java.util.HashSet;
067import java.util.Iterator;
068import java.util.List;
069import java.util.Set;
070
071
072/**
073 * A WorkflowEngine implementation which runs simulations.  This object is not thread-safe
074 * and therefore a new instance needs to be instantiated on every use.
075 *
076 * @author Kuali Rice Team (rice.collab@kuali.org)
077 */
078public class SimulationEngine extends StandardWorkflowEngine implements SimulationWorkflowEngine {
079
080    public SimulationEngine () {
081        super();
082    }
083    public SimulationEngine(RouteNodeService routeNodeService, RouteHeaderService routeHeaderService,
084            ParameterService parameterService, OrchestrationConfig config) {
085        super(routeNodeService, routeHeaderService, parameterService, config);
086    }
087
088    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(SimulationEngine.class);
089
090        private SimulationCriteria criteria;
091    private SimulationResults results;
092
093    @Override
094    public SimulationResults runSimulation(SimulationCriteria criteria) throws Exception {
095        try {
096            this.criteria = criteria;
097            this.results = new SimulationResults();
098            validateCriteria(criteria);
099            process(criteria.getDocumentId(), null);
100            return results;
101        } finally {
102            //nulling out the results & criteria since these really should only be local variables.
103            this.criteria = null;
104            this.results = null;
105        }
106    }
107
108    @Override
109    public void process(String documentId, String nodeInstanceId) throws InvalidActionTakenException, DocumentSimulatedRouteException {
110        RouteContext context = RouteContext.createNewRouteContext();
111        try {
112                ActivationContext activationContext = new ActivationContext(ActivationContext.CONTEXT_IS_SIMULATION);
113                if (criteria.isActivateRequests() == null) {
114                    activationContext.setActivateRequests(!criteria.getActionsToTake().isEmpty());
115                } else {
116                    activationContext.setActivateRequests(criteria.isActivateRequests().booleanValue());
117                }
118                context.setActivationContext(activationContext);
119                context.setEngineState(new EngineState());
120                // suppress policy errors when running a simulation for the purposes of display on the route log
121                RequestsNode.setSupressPolicyErrors(context);
122                DocumentRouteHeaderValue document = createSimulationDocument(documentId, criteria, context);
123            document.setInitiatorWorkflowId("simulation");
124                if ( (criteria.isDocumentSimulation()) && ( (document.isProcessed()) || (document.isFinal()) ) ) {
125                        results.setDocument(document);
126                        return;
127                }
128                routeDocumentIfNecessary(document, criteria, context);
129                results.setDocument(document);
130                documentId = document.getDocumentId();
131                
132                // detect if MDC already has docId param (to avoid nuking it below)
133                boolean mdcHadDocId = MDC.get("docId") != null;
134                if (!mdcHadDocId) { MDC.put("docId", documentId); }
135                
136                PerformanceLogger perfLog = new PerformanceLogger(documentId);
137                try {
138                    if ( LOG.isInfoEnabled() ) {
139                        LOG.info("Processing document for Simulation: " + documentId);
140                    }
141                        List<RouteNodeInstance> activeNodeInstances = getRouteNodeService().getActiveNodeInstances(document);
142                        List<RouteNodeInstance> nodeInstancesToProcess = determineNodeInstancesToProcess(activeNodeInstances, criteria.getDestinationNodeName());
143
144                        context.setDocument(document);
145                        // TODO set document content
146                        context.setEngineState(new EngineState());
147                        ProcessContext processContext = new ProcessContext(true, nodeInstancesToProcess);
148                        while (! nodeInstancesToProcess.isEmpty()) {
149                                RouteNodeInstance nodeInstance = (RouteNodeInstance)nodeInstancesToProcess.remove(0);
150                                if ( !nodeInstance.isActive() ) {
151                                        continue;
152                                }
153                                NodeJotter.jotNodeInstance(context.getDocument(), nodeInstance);
154                                context.setNodeInstance(nodeInstance);
155                                processContext = processNodeInstance(context, helper);
156                                if (!hasReachedCompletion(processContext, context.getEngineState().getGeneratedRequests(), nodeInstance, criteria)) {
157                                        if (processContext.isComplete()) {
158                                                if (!processContext.getNextNodeInstances().isEmpty()) {
159                                                        nodeInstancesToProcess.addAll(processContext.getNextNodeInstances());
160                                                }
161                                                context.getActivationContext().getSimulatedActionsTaken().addAll(processPotentialActionsTaken(context, document, nodeInstance, criteria));
162                                        }
163                                } else {
164                                        context.getActivationContext().getSimulatedActionsTaken().addAll(processPotentialActionsTaken(context, document, nodeInstance, criteria));
165                                }
166                        }
167                        List simulatedActionRequests = context.getEngineState().getGeneratedRequests();
168                        Collections.sort(simulatedActionRequests, new Utilities.RouteLogActionRequestSorter());
169                        results.setSimulatedActionRequests(simulatedActionRequests);
170                        results.setSimulatedActionsTaken(context.getActivationContext().getSimulatedActionsTaken());
171            } catch (InvalidActionTakenException e) {
172                throw e;
173            } catch (Exception e) {
174                String errorMsg = "Error running simulation for document " + ((criteria.isDocumentSimulation()) ? "id " + documentId.toString() : "type " + criteria.getDocumentTypeName());
175                LOG.error(errorMsg,e);
176                throw new DocumentSimulatedRouteException(errorMsg, e);
177                } finally {
178                        perfLog.log("Time to run simulation.");
179                        RouteContext.clearCurrentRouteContext();
180                        
181                        if (!mdcHadDocId) { MDC.remove("docID"); }
182                }
183        } finally {
184                RouteContext.releaseCurrentRouteContext();
185        }
186    }
187
188    /**
189     * If there are multiple paths, we need to figure out which ones we need to follow for blanket approval.
190     * This method will throw an exception if a node with the given name could not be located in the routing path.
191     * This method is written in such a way that it should be impossible for there to be an infinate loop, even if
192     * there is extensive looping in the node graph.
193     */
194    private List<RouteNodeInstance> determineNodeInstancesToProcess(List<RouteNodeInstance> activeNodeInstances, String nodeName) throws InvalidActionTakenException {
195        if (org.apache.commons.lang.StringUtils.isEmpty(nodeName)) {
196            return activeNodeInstances;
197        }
198        List<RouteNodeInstance> nodeInstancesToProcess = new ArrayList<RouteNodeInstance>();
199        for (RouteNodeInstance nodeInstance : activeNodeInstances) {
200            if (nodeName.equals(nodeInstance.getName())) {
201                // one of active node instances is node instance to stop at
202                return new ArrayList<RouteNodeInstance>();
203            } else {
204                if (isNodeNameInPath(nodeName, nodeInstance)) {
205                    nodeInstancesToProcess.add(nodeInstance);
206                }
207            }
208        }
209        if (nodeInstancesToProcess.size() == 0) {
210            throw new InvalidActionTakenException("Could not locate a node with the given name in the blanket approval path '" + nodeName + "'.  " +
211                    "The document is probably already passed the specified node or does not contain the node.");
212        }
213        return nodeInstancesToProcess;
214    }
215
216    private boolean isNodeNameInPath(String nodeName, RouteNodeInstance nodeInstance) {
217        boolean isInPath = false;
218        for (Iterator<RouteNode> iterator = nodeInstance.getRouteNode().getNextNodes().iterator(); iterator.hasNext();) {
219            RouteNode nextNode = (RouteNode) iterator.next();
220            isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, new HashSet<String>());
221        }
222        return isInPath;
223    }
224
225    private boolean isNodeNameInPath(String nodeName, RouteNode node, Set<String> inspected) {
226        boolean isInPath = !inspected.contains(node.getRouteNodeId()) && node.getRouteNodeName().equals(nodeName);
227        inspected.add(node.getRouteNodeId());
228        if (helper.isSubProcessNode(node)) {
229            ProcessDefinitionBo subProcess = node.getDocumentType().getNamedProcess(node.getRouteNodeName());
230            RouteNode subNode = subProcess.getInitialRouteNode();
231            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}