001/**
002 * Copyright 2005-2016 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;
017
018import org.apache.log4j.MDC;
019import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
020import org.kuali.rice.coreservice.framework.parameter.ParameterService;
021import org.kuali.rice.kew.actionrequest.ActionRequestValue;
022import org.kuali.rice.kew.api.doctype.IllegalDocumentTypeException;
023import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
024import org.kuali.rice.kew.api.exception.WorkflowException;
025import org.kuali.rice.kew.engine.node.Branch;
026import org.kuali.rice.kew.engine.node.BranchState;
027import org.kuali.rice.kew.engine.node.ProcessDefinitionBo;
028import org.kuali.rice.kew.engine.node.ProcessResult;
029import org.kuali.rice.kew.engine.node.RouteNodeInstance;
030import org.kuali.rice.kew.engine.node.RouteNodeUtils;
031import org.kuali.rice.kew.engine.node.service.RouteNodeService;
032import org.kuali.rice.kew.engine.transition.Transition;
033import org.kuali.rice.kew.engine.transition.TransitionEngine;
034import org.kuali.rice.kew.engine.transition.TransitionEngineFactory;
035import org.kuali.rice.kew.exception.RouteManagerException;
036import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent;
037import org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent;
038import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent;
039import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange;
040import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
041import org.kuali.rice.kew.framework.postprocessor.PostProcessor;
042import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
043import org.kuali.rice.kew.postprocessor.DefaultPostProcessor;
044import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
045import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
046import org.kuali.rice.kew.service.KEWServiceLocator;
047import org.kuali.rice.kew.api.KewApiConstants;
048import org.kuali.rice.kew.util.PerformanceLogger;
049import org.kuali.rice.krad.util.KRADConstants;
050
051import java.sql.Timestamp;
052import java.util.ArrayList;
053import java.util.Collection;
054import java.util.Iterator;
055import java.util.LinkedList;
056import java.util.List;
057
058
059/**
060 * The standard and supported implementation of the WorkflowEngine.  Runs a processing loop against a given
061 * Document, processing nodes on the document until the document is completed or a node halts the
062 * processing.
063 *
064 * @author Kuali Rice Team (rice.collab@kuali.org)
065 */
066public class StandardWorkflowEngine implements WorkflowEngine {
067
068        private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardWorkflowEngine.class);
069
070        protected final RouteHelper helper = new RouteHelper();
071        protected RouteNodeService routeNodeService;
072    protected RouteHeaderService routeHeaderService;
073    protected ParameterService parameterService;
074    protected OrchestrationConfig config;
075
076    public StandardWorkflowEngine() {}
077
078        protected StandardWorkflowEngine(RouteNodeService routeNodeService, RouteHeaderService routeHeaderService, 
079                ParameterService parameterService, OrchestrationConfig config) {
080            this.routeNodeService = routeNodeService;
081            this.routeHeaderService = routeHeaderService;
082            this.parameterService = parameterService;
083            this.config = config;
084        }
085
086//      public void setRunPostProcessorLogic(boolean runPostProcessorLogic) {
087//          this.runPostProcessorLogic = runPostProcessorLogic;
088//      }
089
090        public boolean isRunPostProcessorLogic() {
091            return this.config.isRunPostProcessorLogic();
092        }
093
094        public void process(String documentId, String nodeInstanceId) throws Exception {
095                if (documentId == null) {
096                        throw new IllegalArgumentException("Cannot process a null document id.");
097                }
098                MDC.put("docId", documentId);
099                boolean success = true;
100                RouteContext context = RouteContext.createNewRouteContext();
101                try {
102                        if ( LOG.isInfoEnabled() ) {
103                                LOG.info("Aquiring lock on document " + documentId);
104                        }
105                        KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId, true);
106                        if ( LOG.isInfoEnabled() ) {
107                                LOG.info("Aquired lock on document " + documentId);
108                        }
109
110                        DocumentRouteHeaderValue document = getRouteHeaderService().getRouteHeader(documentId);
111                        context.setDocument(document);
112                        lockAdditionalDocuments(document);
113
114                        if ( LOG.isInfoEnabled() ) {
115                                LOG.info("Processing document: " + documentId + " : " + nodeInstanceId);
116                        }
117
118                        try {
119                    document = notifyPostProcessorBeforeProcess(document, nodeInstanceId);
120                    context.setDocument(document);
121            } catch (Exception e) {
122                LOG.warn("Problems contacting PostProcessor before engine process", e);
123                throw new RouteManagerException("Problems contacting PostProcessor:  " + e.getMessage());
124            }
125            if (!document.isRoutable()) {
126                                LOG.debug("Document not routable so returning with doing no action");
127                                return;
128                        }
129                        List<RouteNodeInstance> nodeInstancesToProcess = new LinkedList<RouteNodeInstance>();
130                        if (nodeInstanceId == null) {
131                                // pulls the node instances from the passed in document
132                                nodeInstancesToProcess.addAll(RouteNodeUtils.getActiveNodeInstances(document));
133                        } else {
134                                RouteNodeInstance instanceNode = RouteNodeUtils.findRouteNodeInstanceById(nodeInstanceId,document);
135                                if (instanceNode == null) {
136                                        throw new IllegalArgumentException("Invalid node instance id: " + nodeInstanceId);
137                                }
138                                nodeInstancesToProcess.add(instanceNode);
139                        }
140
141                        context.setEngineState(new EngineState());
142                        ProcessContext processContext = new ProcessContext(true, nodeInstancesToProcess);
143                        try {
144                                while (!nodeInstancesToProcess.isEmpty()) {
145                                        context.setNodeInstance((RouteNodeInstance) nodeInstancesToProcess.remove(0));
146                                        processContext = processNodeInstance(context, helper);
147                                        if (processContext.isComplete() && !processContext.getNextNodeInstances().isEmpty()) {
148                                                nodeInstancesToProcess.addAll(processContext.getNextNodeInstances());
149                                        }
150                                }
151                                context.setDocument(nodePostProcess(context));
152                        } catch (Exception e) {
153                                success = false;
154                                // TODO throw a new 'RoutingException' which holds the
155                                // RoutingState
156                                throw new RouteManagerException(e, context);
157                        }
158                } finally {
159                        if ( LOG.isInfoEnabled() ) {
160                                LOG.info((success ? "Successfully processed" : "Failed to process") + " document: " + documentId + " : " + nodeInstanceId);
161                        }
162                        try {
163                    notifyPostProcessorAfterProcess(context.getDocument(), nodeInstanceId, success);
164            } catch (Exception e) {
165                LOG.warn("Problems contacting PostProcessor after engine process", e);
166                throw new RouteManagerException("Problems contacting PostProcessor", e, context);
167            }
168                        RouteContext.clearCurrentRouteContext();
169                        MDC.remove("docId");
170                }
171        }
172
173        protected ProcessContext processNodeInstance(RouteContext context, RouteHelper helper) throws Exception {
174                RouteNodeInstance nodeInstance = context.getNodeInstance();
175                if ( LOG.isDebugEnabled() ) {
176                        LOG.debug("Processing node instance: " + nodeInstance.getRouteNode().getRouteNodeName());
177                }
178                if (checkAssertions(context)) {
179                        // returning an empty context causes the outer loop to terminate
180                        return new ProcessContext();
181                }
182                TransitionEngine transitionEngine = TransitionEngineFactory.createTransitionEngine(nodeInstance);
183                ProcessResult processResult = transitionEngine.isComplete(context);
184                nodeInstance.setInitial(false);
185
186                // if this nodeInstance already has next node instance we don't need to
187                // go to the TE
188                if (processResult.isComplete()) {
189                        if ( LOG.isDebugEnabled() ) {
190                                LOG.debug("Routing node has completed: " + nodeInstance.getRouteNode().getRouteNodeName());
191                        }
192
193                        context.getEngineState().getCompleteNodeInstances().add(nodeInstance.getRouteNodeInstanceId());
194                        List nextNodeCandidates = invokeTransition(context, context.getNodeInstance(), processResult, transitionEngine);
195
196                        // iterate over the next node candidates sending them through the
197                        // transition engine's transitionTo method
198                        // one at a time for a potential switch. Place the transition
199                        // engines result back in the 'actual' next node
200                        // list which we put in the next node before doing work.
201                        List<RouteNodeInstance> nodesToActivate = new ArrayList<RouteNodeInstance>();
202                        if (!nextNodeCandidates.isEmpty()) {
203                                // KULRICE-4274: Hierarchy Routing Node issues
204                                // No longer change nextNodeInstances in place, instead we create a local and assign our local list below
205                                // the loop so the post processor doesn't save a RouteNodeInstance in an intermediate state
206                                ArrayList<RouteNodeInstance> nextNodeInstances = new ArrayList<RouteNodeInstance>();
207
208                                for (Iterator nextIt = nextNodeCandidates.iterator(); nextIt.hasNext();) {
209                                        RouteNodeInstance nextNodeInstance = (RouteNodeInstance) nextIt.next();
210                                        transitionEngine = TransitionEngineFactory.createTransitionEngine(nextNodeInstance);
211                                        RouteNodeInstance currentNextNodeInstance = nextNodeInstance;
212                                        nextNodeInstance = transitionEngine.transitionTo(nextNodeInstance, context);
213                                        // if the next node has changed, we need to remove our
214                                        // current node as a next node of the original node
215                                        if (!currentNextNodeInstance.equals(nextNodeInstance)) {
216                                                currentNextNodeInstance.getPreviousNodeInstances().remove(nodeInstance);
217                                        }
218                                        // before adding next node instance, be sure that it's not
219                                        // already linked via previous node instances
220                                        // this is to prevent the engine from setting up references
221                                        // on nodes that already reference each other.
222                                        // the primary case being when we are walking over an
223                                        // already constructed graph of nodes returned from a
224                                        // dynamic node - probably a more sensible approach would be
225                                        // to check for the existence of the link and moving on
226                                        // if it's been established.
227                                        nextNodeInstance.getPreviousNodeInstances().remove(nodeInstance);
228                                        nextNodeInstances.add(nextNodeInstance);
229                                        handleBackwardCompatibility(context, nextNodeInstance);
230                                        // call the post processor
231                                        notifyNodeChange(context, nextNodeInstance);
232                                        nodesToActivate.add(nextNodeInstance);
233                                        // TODO update document content on context?
234                                }
235                                // assign our local list here so the post processor doesn't save a RouteNodeInstance in an intermediate state
236                                for (RouteNodeInstance nextNodeInstance : nextNodeInstances) {
237                                        nodeInstance.addNextNodeInstance(nextNodeInstance);
238                                }
239                        }
240 
241                        // deactive the current active node
242                        nodeInstance.setComplete(true);
243                        nodeInstance.setActive(false);
244                        // active the nodes we're transitioning into
245                        for (RouteNodeInstance nodeToActivate : nodesToActivate) {
246                                nodeToActivate.setActive(true);
247                        }
248                } else {
249                    nodeInstance.setComplete(false);
250        }
251
252                saveNode(context, nodeInstance);
253                return new ProcessContext(nodeInstance.isComplete(), nodeInstance.getNextNodeInstances());
254        }
255
256        /**
257         * Checks various assertions regarding the processing of the current node.
258         * If this method returns true, then the node will not be processed.
259         *
260         * This method will throw an exception if it deems that the processing is in
261         * a illegal state.
262         */
263        private boolean checkAssertions(RouteContext context) throws Exception {
264                if (context.getNodeInstance().isComplete()) {
265                        if ( LOG.isDebugEnabled() ) {
266                                LOG.debug("The node has already been completed: " + context.getNodeInstance().getRouteNode().getRouteNodeName());
267                        }
268                        return true;
269                }
270                if (isRunawayProcessDetected(context.getEngineState())) {
271//                       TODO more info in message
272                        throw new WorkflowException("Detected runaway process.");
273                }
274                return false;
275        }
276
277        /**
278         * Invokes the transition and returns the next node instances to transition
279         * to from the current node instance on the route context.
280         *
281         * This is a 3-step process:
282         *
283         * <pre>
284         *  1) If the node instance already has next nodes, return those,
285         *  2) otherwise, invoke the transition engine for the node, if the resulting node instances are not empty, return those,
286         *  3) lastly, if our node is in a process and no next nodes were returned from it's transition engine, invoke the
287         *     transition engine of the process node and return the resulting node instances.
288         * </pre>
289         */
290        /*
291         * private List invokeTransition(RouteContext context, RouteNodeInstance
292         * nodeInstance, ProcessResult processResult, TransitionEngine
293         * transitionEngine) throws Exception { List nextNodeInstances =
294         * nodeInstance.getNextNodeInstances(); if (nextNodeInstances.isEmpty()) {
295         * Transition result = transitionEngine.transitionFrom(context,
296         * processResult); nextNodeInstances = result.getNextNodeInstances(); if
297         * (nextNodeInstances.isEmpty() && nodeInstance.isInProcess()) {
298         * transitionEngine =
299         * TransitionEngineFactory.createTransitionEngine(nodeInstance.getProcess());
300         * nextNodeInstances = invokeTransition(context, nodeInstance.getProcess(),
301         * processResult, transitionEngine); } } return nextNodeInstances; }
302         */
303
304        private List invokeTransition(RouteContext context, RouteNodeInstance nodeInstance, ProcessResult processResult, TransitionEngine transitionEngine) throws Exception {
305                List nextNodeInstances = nodeInstance.getNextNodeInstances();
306                if (nextNodeInstances.isEmpty()) {
307                        Transition result = transitionEngine.transitionFrom(context, processResult);
308                        nextNodeInstances = result.getNextNodeInstances();
309                        if (nextNodeInstances.isEmpty() && nodeInstance.isInProcess()) {
310                                transitionEngine = TransitionEngineFactory.createTransitionEngine(nodeInstance.getProcess());
311                                context.setNodeInstance(nodeInstance);
312                                nextNodeInstances = invokeTransition(context, nodeInstance.getProcess(), processResult, transitionEngine);
313                        }
314                }
315                return nextNodeInstances;
316        }
317
318        /*
319         * private List invokeTransition(RouteContext context, RouteNodeInstance
320         * process, ProcessResult processResult) throws Exception {
321         * RouteNodeInstance nodeInstance = (context.getRouteNodeInstance() ; List
322         * nextNodeInstances = nodeInstance.getNextNodeInstances(); if
323         * (nextNodeInstances.isEmpty()) { TransitionEngine transitionEngine =
324         * TransitionEngineFactory.createTransitionEngine(nodeInstance); Transition
325         * result = transitionEngine.transitionFrom(context, processResult);
326         * nextNodeInstances = result.getNextNodeInstances(); if
327         * (nextNodeInstances.isEmpty() && nodeInstance.isInProcess()) {
328         * transitionEngine =
329         * TransitionEngineFactory.createTransitionEngine(nodeInstance.getProcess());
330         * nextNodeInstances = invokeTransition(context, nodeInstance.getProcess(),
331         * processResult, transitionEngine); } } return nextNodeInstances; }
332         *
333         */private void notifyNodeChange(RouteContext context, RouteNodeInstance nextNodeInstance) throws Exception {
334                if (!context.isSimulation()) {
335                        RouteNodeInstance nodeInstance = context.getNodeInstance();
336                        // if application document status transition has been defined, update the status
337                        String nextStatus = nodeInstance.getRouteNode().getNextDocStatus();
338                        if (nextStatus != null && nextStatus.length() > 0){
339                                context.getDocument().updateAppDocStatus(nextStatus);
340                        }
341
342                        DocumentRouteLevelChange event = new DocumentRouteLevelChange(context.getDocument().getDocumentId(), context.getDocument().getAppDocId(), CompatUtils.getLevelForNode(context.getDocument().getDocumentType(), context.getNodeInstance()
343                                        .getRouteNode().getRouteNodeName()), CompatUtils.getLevelForNode(context.getDocument().getDocumentType(), nextNodeInstance.getRouteNode().getRouteNodeName()), nodeInstance.getRouteNode().getRouteNodeName(), nextNodeInstance
344                                        .getRouteNode().getRouteNodeName(), nodeInstance.getRouteNodeInstanceId(), nextNodeInstance.getRouteNodeInstanceId());
345                        context.setDocument(notifyPostProcessor(context.getDocument(), nodeInstance, event));
346                }
347        }
348
349        private void handleBackwardCompatibility(RouteContext context, RouteNodeInstance nextNodeInstance) {
350                context.getDocument().setDocRouteLevel(new Integer(context.getDocument().getDocRouteLevel().intValue() + 1)); // preserve
351                                                                                                                                                                                                                                                // route
352                                                                                                                                                                                                                                                // level
353                                                                                                                                                                                                                                                // concept
354                                                                                                                                                                                                                                                // if
355                                                                                                                                                                                                                                                // possible
356                saveDocument(context);
357        }
358
359        private void saveDocument(RouteContext context) {
360                if (!context.isSimulation()) {
361                        getRouteHeaderService().saveRouteHeader(context.getDocument());
362                }
363        }
364
365        private void saveBranch(RouteContext context, Branch branch) {
366                if (!context.isSimulation()) {
367                        KEWServiceLocator.getRouteNodeService().save(branch);
368                }
369        }
370
371        protected void saveNode(RouteContext context, RouteNodeInstance nodeInstance) {
372                if (!context.isSimulation()) {
373                        getRouteNodeService().save(nodeInstance);
374                } else {
375                        // if we are in simulation mode, lets go ahead and assign some id
376                        // values to our beans
377                        for (Iterator<RouteNodeInstance> iterator = nodeInstance.getNextNodeInstances().iterator(); iterator.hasNext();) {
378                                RouteNodeInstance routeNodeInstance = (RouteNodeInstance) iterator.next();
379                                if (routeNodeInstance.getRouteNodeInstanceId() == null) {
380                                        routeNodeInstance.setRouteNodeInstanceId(context.getEngineState().getNextSimulationId());
381                                }
382                        }
383                        if (nodeInstance.getProcess() != null && nodeInstance.getProcess().getRouteNodeInstanceId() == null) {
384                                nodeInstance.getProcess().setRouteNodeInstanceId(context.getEngineState().getNextSimulationId());
385                        }
386                        if (nodeInstance.getBranch() != null && nodeInstance.getBranch().getBranchId() == null) {
387                                nodeInstance.getBranch().setBranchId(context.getEngineState().getNextSimulationId());
388                        }
389                }
390        }
391
392        // TODO extract this into some sort of component which handles transitioning
393        // document state
394        protected DocumentRouteHeaderValue nodePostProcess(RouteContext context) throws InvalidActionTakenException {
395                DocumentRouteHeaderValue document = context.getDocument();
396        Collection<RouteNodeInstance> activeNodes = RouteNodeUtils.getActiveNodeInstances(document);
397        boolean moreNodes = false;
398                for (Iterator<RouteNodeInstance> iterator = activeNodes.iterator(); iterator.hasNext();) {
399                        RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator.next();
400                        moreNodes = moreNodes || !nodeInstance.isComplete();
401                }
402                List pendingRequests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId());
403                boolean activeApproveRequests = false;
404                boolean activeAckRequests = false;
405                for (Iterator iterator = pendingRequests.iterator(); iterator.hasNext();) {
406                        ActionRequestValue request = (ActionRequestValue) iterator.next();
407                        activeApproveRequests = request.isApproveOrCompleteRequest() || activeApproveRequests;
408                        activeAckRequests = request.isAcknowledgeRequest() || activeAckRequests;
409                }
410                // TODO is the logic for going processed still going to be valid?
411                if (!document.isProcessed() && (!moreNodes || !activeApproveRequests)) {
412                        if ( LOG.isDebugEnabled() ) {
413                                LOG.debug("No more nodes for this document " + document.getDocumentId());
414                        }
415                        // TODO perhaps the policies could also be factored out?
416                        checkDefaultApprovalPolicy(document);
417            document.setApprovedDate(new Timestamp(System.currentTimeMillis()));
418
419                        LOG.debug("Marking document processed");
420                        DocumentRouteStatusChange event = new DocumentRouteStatusChange(document.getDocumentId(), document.getAppDocId(), document.getDocRouteStatus(), KewApiConstants.ROUTE_HEADER_PROCESSED_CD);
421                        document.markDocumentProcessed();
422                        // saveDocument(context);
423                        notifyPostProcessor(context, event);
424                }
425
426                // if document is processed and no pending action requests put the
427                // document into the finalized state.
428                if (document.isProcessed()) {
429                        DocumentRouteStatusChange event = new DocumentRouteStatusChange(document.getDocumentId(), document.getAppDocId(), document.getDocRouteStatus(), KewApiConstants.ROUTE_HEADER_FINAL_CD);
430                        List actionRequests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId());
431                        if (actionRequests.isEmpty()) {
432                                document.markDocumentFinalized();
433                                // saveDocument(context);
434                                notifyPostProcessor(context, event);
435                        } else {
436                                boolean markFinalized = true;
437                                for (Iterator iter = actionRequests.iterator(); iter.hasNext();) {
438                                        ActionRequestValue actionRequest = (ActionRequestValue) iter.next();
439                                        if (KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ.equals(actionRequest.getActionRequested())) {
440                                                markFinalized = false;
441                                        }
442                                }
443                                if (markFinalized) {
444                                        document.markDocumentFinalized();
445                                        // saveDocument(context);
446                                        this.notifyPostProcessor(context, event);
447                                }
448                        }
449                }
450                saveDocument(context);
451                return document;
452        }
453
454        /**
455         * Check the default approval policy for the document. If the default
456         * approval policy is no and no approval action requests have been created
457         * then throw an execption so that the document will get thrown into
458         * exception routing.
459         *
460         * @throws RouteManagerException
461         */
462        private void checkDefaultApprovalPolicy(DocumentRouteHeaderValue document) throws RouteManagerException {
463                if (!document.getDocumentType().getDefaultApprovePolicy().getPolicyValue().booleanValue()) {
464                        LOG.debug("Checking if any requests have been generated for the document");
465                        List requests = KEWServiceLocator.getActionRequestService().findAllActionRequestsByDocumentId(document.getDocumentId());
466                        boolean approved = false;
467                        for (Iterator iter = requests.iterator(); iter.hasNext();) {
468                                ActionRequestValue actionRequest = (ActionRequestValue) iter.next();
469                                if (actionRequest.isApproveOrCompleteRequest() && actionRequest.isDone()) { // &&
470                                                                                                                                                                                        // !(actionRequest.getRouteMethodName().equals(KewApiConstants.ADHOC_ROUTE_MODULE_NAME)
471                                                                                                                                                                                        // &&
472                                                                                                                                                                                        // actionRequest.isReviewerUser()
473                                                                                                                                                                                        // &&
474                                                                                                                                                                                        // document.getInitiatorWorkflowId().equals(actionRequest.getWorkflowId())))
475                                                                                                                                                                                        // {
476                                        LOG.debug("Found at least one processed approve request so document can be approved");
477                                        approved = true;
478                                        break;
479                                }
480                        }
481                        if (!approved) {
482                                LOG.debug("Document requires at least one request and none are present");
483                                // TODO what route method name to pass to this?
484                                throw new RouteManagerException("Document should have generated at least one approval request.");
485                        }
486                }
487        }
488
489        private DocumentRouteHeaderValue notifyPostProcessor(RouteContext context, DocumentRouteStatusChange event) {
490                DocumentRouteHeaderValue document = context.getDocument();
491                if (context.isSimulation()) {
492                        return document;
493                }
494                if (hasContactedPostProcessor(context, event)) {
495                        return document;
496                }
497                String documentId = event.getDocumentId();
498                PerformanceLogger performanceLogger = new PerformanceLogger(documentId);
499                ProcessDocReport processReport = null;
500                PostProcessor postProc = null;
501        try {
502            // use the document's post processor unless specified by the runPostProcessorLogic not to
503            if (!isRunPostProcessorLogic()) {
504                postProc = new DefaultPostProcessor();
505            } else {
506                postProc = document.getDocumentType().getPostProcessor();
507            }
508        } catch (Exception e) {
509            LOG.error("Error retrieving PostProcessor for document " + document.getDocumentId(), e);
510            throw new RouteManagerException("Error retrieving PostProcessor for document " + document.getDocumentId(), e);
511        }
512                try {
513                        processReport = postProc.doRouteStatusChange(event);
514                } catch (Exception e) {
515                        LOG.error("Error notifying post processor", e);
516                        throw new RouteManagerException(KewApiConstants.POST_PROCESSOR_FAILURE_MESSAGE, e);
517                } finally {
518                        performanceLogger.log("Time to notifyPostProcessor of event " + event.getDocumentEventCode() + ".");
519                }
520
521                if (!processReport.isSuccess()) {
522                        LOG.warn("PostProcessor failed to process document: " + processReport.getMessage());
523                        throw new RouteManagerException(KewApiConstants.POST_PROCESSOR_FAILURE_MESSAGE + processReport.getMessage());
524                }
525                return document;
526        }
527
528        /**
529         * Returns true if the post processor has already been contacted about a
530         * PROCESSED or FINAL post processor change. If the post processor has not
531         * been contacted, this method will record on the document that it has been.
532         *
533         * This is because, in certain cases, a document could end up in exception
534         * routing after it has already gone PROCESSED or FINAL (i.e. on Mass Action
535         * processing) and we don't want to re-contact the post processor in these
536         * cases.
537         */
538        private boolean hasContactedPostProcessor(RouteContext context, DocumentRouteStatusChange event) {
539                // get the initial node instance, the root branch is where we will store
540                // the state
541                Branch rootBranch = context.getDocument().getRootBranch();
542                String key = null;
543                if (KewApiConstants.ROUTE_HEADER_PROCESSED_CD.equals(event.getNewRouteStatus())) {
544                        key = KewApiConstants.POST_PROCESSOR_PROCESSED_KEY;
545                } else if (KewApiConstants.ROUTE_HEADER_FINAL_CD.equals(event.getNewRouteStatus())) {
546                        key = KewApiConstants.POST_PROCESSOR_FINAL_KEY;
547                } else {
548                        return false;
549                }
550                BranchState branchState = null;
551                if (rootBranch != null) {
552                    branchState = rootBranch.getBranchState(key);
553                } else {
554                    return false;
555                }
556                if (branchState == null) {
557                        branchState = new BranchState();
558                        branchState.setKey(key);
559                        branchState.setValue("true");
560                        rootBranch.addBranchState(branchState);
561                        saveBranch(context, rootBranch);
562                        return false;
563                }
564                return "true".equals(branchState.getValue());
565        }
566
567        /**
568         * TODO in some cases, someone may modify the route header in the post
569         * processor, if we don't save before and reload after we will get an
570         * optimistic lock exception, we need to work on a better solution for this!
571         * TODO get the routeContext in this method - it should be a better object
572         * than the nodeInstance
573         */
574        private DocumentRouteHeaderValue notifyPostProcessor(DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance, DocumentRouteLevelChange event) {
575                getRouteHeaderService().saveRouteHeader(document);
576                ProcessDocReport report = null;
577                try {
578                PostProcessor postProcessor = null;
579                // use the document's post processor unless specified by the runPostProcessorLogic not to
580                if (!isRunPostProcessorLogic()) {
581                    postProcessor = new DefaultPostProcessor();
582                } else {
583                    postProcessor = document.getDocumentType().getPostProcessor();
584                }
585                        report = postProcessor.doRouteLevelChange(event);
586                } catch (Exception e) {
587                        LOG.warn("Problems contacting PostProcessor", e);
588                        throw new RouteManagerException("Problems contacting PostProcessor:  " + e.getMessage());
589                }
590                document = getRouteHeaderService().getRouteHeader(document.getDocumentId());
591                if (!report.isSuccess()) {
592                        LOG.error("PostProcessor rejected route level change::" + report.getMessage(), report.getProcessException());
593                        throw new RouteManagerException("Route Level change failed in post processor::" + report.getMessage());
594                }
595                return document;
596        }
597
598    /**
599     * TODO get the routeContext in this method - it should be a better object
600     * than the nodeInstance
601     */
602        private DocumentRouteHeaderValue notifyPostProcessorBeforeProcess(DocumentRouteHeaderValue document, String nodeInstanceId) {
603            return notifyPostProcessorBeforeProcess(document, nodeInstanceId, new BeforeProcessEvent(document.getDocumentId(),document.getAppDocId(),nodeInstanceId));
604        }
605
606    /**
607     * TODO get the routeContext in this method - it should be a better object
608     * than the nodeInstance
609     */
610    private DocumentRouteHeaderValue notifyPostProcessorBeforeProcess(DocumentRouteHeaderValue document, String nodeInstanceId, BeforeProcessEvent event) {
611        ProcessDocReport report = null;
612        try {
613            PostProcessor postProcessor = null;
614            // use the document's post processor unless specified by the runPostProcessorLogic not to
615            if (!isRunPostProcessorLogic()) {
616                postProcessor = new DefaultPostProcessor();
617            } else {
618                postProcessor = document.getDocumentType().getPostProcessor();
619            }
620            report = postProcessor.beforeProcess(event);
621        } catch (Exception e) {
622            LOG.warn("Problems contacting PostProcessor", e);
623            throw new RouteManagerException("Problems contacting PostProcessor:  " + e.getMessage());
624        }
625        document = getRouteHeaderService().getRouteHeader(document.getDocumentId());
626        if (!report.isSuccess()) {
627            LOG.error("PostProcessor rejected route level change::" + report.getMessage(), report.getProcessException());
628            throw new RouteManagerException("Route Level change failed in post processor::" + report.getMessage());
629        }
630        return document;
631    }
632
633    protected void lockAdditionalDocuments(DocumentRouteHeaderValue document) throws Exception {
634                DocumentLockingEvent lockingEvent = new DocumentLockingEvent(document.getDocumentId(), document.getAppDocId());
635                // TODO this shows up in a few places and could totally be extracted to a method
636                PostProcessor postProcessor = null;
637        // use the document's post processor unless specified by the runPostProcessorLogic not to
638        if (!isRunPostProcessorLogic()) {
639            postProcessor = new DefaultPostProcessor();
640        } else {
641            postProcessor = document.getDocumentType().getPostProcessor();
642        }
643        List<String> documentIdsToLock = postProcessor.getDocumentIdsToLock(lockingEvent);
644        if (documentIdsToLock != null && !documentIdsToLock.isEmpty()) {
645                for (String documentId : documentIdsToLock) {
646                        if ( LOG.isInfoEnabled() ) {
647                                LOG.info("Aquiring additional lock on document " + documentId);
648                        }
649                        getRouteHeaderService().lockRouteHeader(documentId, true);
650                        if ( LOG.isInfoEnabled() ) {
651                                LOG.info("Aquired lock on document " + documentId);
652                        }
653                }
654        }
655        }
656
657    /**
658     * TODO get the routeContext in this method - it should be a better object
659     * than the nodeInstance
660     */
661    private DocumentRouteHeaderValue notifyPostProcessorAfterProcess(DocumentRouteHeaderValue document, String nodeInstanceId, boolean successfullyProcessed) {
662        if (document == null) {
663                // this could happen if we failed to acquire the lock on the document
664                return null;
665        }
666        return notifyPostProcessorAfterProcess(document, nodeInstanceId, new AfterProcessEvent(document.getDocumentId(),document.getAppDocId(),nodeInstanceId,successfullyProcessed));
667    }
668
669    /**
670     * TODO get the routeContext in this method - it should be a better object
671     * than the nodeInstance
672     */
673    private DocumentRouteHeaderValue notifyPostProcessorAfterProcess(DocumentRouteHeaderValue document, String nodeInstanceId, AfterProcessEvent event) {
674        ProcessDocReport report = null;
675        try {
676            PostProcessor postProcessor = null;
677            // use the document's post processor unless specified by the runPostProcessorLogic not to
678            if (!isRunPostProcessorLogic()) {
679                postProcessor = new DefaultPostProcessor();
680            } else {
681                postProcessor = document.getDocumentType().getPostProcessor();
682            }
683            report = postProcessor.afterProcess(event);
684        } catch (Exception e) {
685            throw new RouteManagerException("Problems contacting PostProcessor.",e);
686        }
687        document = getRouteHeaderService().getRouteHeader(document.getDocumentId());
688        if (!report.isSuccess()) {
689            LOG.error("PostProcessor rejected route level change::" + report.getMessage(), report.getProcessException());
690            throw new RouteManagerException("Route Level change failed in post processor::" + report.getMessage());
691        }
692        return document;
693    }
694
695        /**
696         * This method initializes the document by materializing and activating the
697         * first node instance on the document.
698         */
699        public void initializeDocument(DocumentRouteHeaderValue document) {
700                // we set up a local route context here just so that we are able to
701                // utilize the saveNode method at the end of
702                // this method. Incidentally, this was changed from pulling the existing
703                // context out because it would override
704                // the document in the route context in the case of a document being
705                // initialized for reporting purposes.
706                RouteContext context = new RouteContext();
707                context.setDocument(document);
708
709                if (context.getEngineState() == null) {
710                        context.setEngineState(new EngineState());
711                }
712
713                ProcessDefinitionBo process = document.getDocumentType().getPrimaryProcess();
714
715        if (process == null) {
716            throw new IllegalDocumentTypeException("DocumentType '" + document.getDocumentType().getName() + "' has no primary process configured!");
717        }
718
719        if (process.getInitialRouteNode() != null) {
720            RouteNodeInstance nodeInstance = helper.getNodeFactory().createRouteNodeInstance(document.getDocumentId(), process.getInitialRouteNode());
721            nodeInstance.setActive(true);
722            helper.getNodeFactory().createBranch(KewApiConstants.PRIMARY_BRANCH_NAME, null, nodeInstance);
723            document.getInitialRouteNodeInstances().add(nodeInstance);
724            saveNode(context, nodeInstance);
725        }
726        }
727
728    private boolean isRunawayProcessDetected(EngineState engineState) throws NumberFormatException {
729            String maxNodesConstant = getParameterService().getParameterValueAsString(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.MAX_NODES_BEFORE_RUNAWAY_PROCESS);
730            int maxNodes = (org.apache.commons.lang.StringUtils.isEmpty(maxNodesConstant)) ? 50 : Integer.valueOf(maxNodesConstant);
731            return engineState.getCompleteNodeInstances().size() > maxNodes;
732        }
733
734    protected RouteNodeService getRouteNodeService() {
735                return routeNodeService;
736        }
737
738        protected RouteHeaderService getRouteHeaderService() {
739                return routeHeaderService;
740        }
741
742    protected ParameterService getParameterService() {
743        if (parameterService == null) {
744            parameterService = CoreFrameworkServiceLocator.getParameterService();
745        }
746                return parameterService;
747        }
748
749    public void setRouteNodeService(RouteNodeService routeNodeService) {
750        this.routeNodeService = routeNodeService;
751    }
752
753    public void setRouteHeaderService(RouteHeaderService routeHeaderService) {
754        this.routeHeaderService = routeHeaderService;
755    }
756
757    public void setParameterService(ParameterService parameterService) {
758        this.parameterService = parameterService;
759    }
760}