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