View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.engine;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.MDC;
20  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
21  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
22  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
23  import org.kuali.rice.kew.api.KewApiConstants;
24  import org.kuali.rice.kew.api.doctype.IllegalDocumentTypeException;
25  import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
26  import org.kuali.rice.kew.api.exception.WorkflowException;
27  import org.kuali.rice.kew.engine.node.Branch;
28  import org.kuali.rice.kew.engine.node.BranchState;
29  import org.kuali.rice.kew.engine.node.ProcessDefinitionBo;
30  import org.kuali.rice.kew.engine.node.ProcessResult;
31  import org.kuali.rice.kew.engine.node.RouteNodeInstance;
32  import org.kuali.rice.kew.engine.node.RouteNodeUtils;
33  import org.kuali.rice.kew.engine.node.service.RouteNodeService;
34  import org.kuali.rice.kew.engine.transition.Transition;
35  import org.kuali.rice.kew.engine.transition.TransitionEngine;
36  import org.kuali.rice.kew.engine.transition.TransitionEngineFactory;
37  import org.kuali.rice.kew.exception.RouteManagerException;
38  import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent;
39  import org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent;
40  import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent;
41  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange;
42  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
43  import org.kuali.rice.kew.framework.postprocessor.PostProcessor;
44  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
45  import org.kuali.rice.kew.postprocessor.DefaultPostProcessor;
46  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
47  import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
48  import org.kuali.rice.kew.service.KEWServiceLocator;
49  import org.kuali.rice.kew.util.PerformanceLogger;
50  import org.kuali.rice.krad.data.KradDataServiceLocator;
51  import org.kuali.rice.krad.util.KRADConstants;
52  
53  import java.sql.Timestamp;
54  import java.util.ArrayList;
55  import java.util.Collection;
56  import java.util.Iterator;
57  import java.util.LinkedList;
58  import java.util.List;
59  
60  
61  /**
62   * The standard and supported implementation of the WorkflowEngine.  Runs a processing loop against a given
63   * Document, processing nodes on the document until the document is completed or a node halts the
64   * processing.
65   *
66   * @author Kuali Rice Team (rice.collab@kuali.org)
67   */
68  public class StandardWorkflowEngine implements WorkflowEngine {
69  
70  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardWorkflowEngine.class);
71  
72  	protected final RouteHelper helper = new RouteHelper();
73  	protected RouteNodeService routeNodeService;
74      protected RouteHeaderService routeHeaderService;
75      protected ParameterService parameterService;
76      protected OrchestrationConfig config;
77  
78      public StandardWorkflowEngine() {}
79  
80  	protected StandardWorkflowEngine(RouteNodeService routeNodeService, RouteHeaderService routeHeaderService, 
81  	        ParameterService parameterService, OrchestrationConfig config) {
82  	    this.routeNodeService = routeNodeService;
83  	    this.routeHeaderService = routeHeaderService;
84  	    this.parameterService = parameterService;
85  	    this.config = config;
86  	}
87  
88  	public boolean isRunPostProcessorLogic() {
89  	    return this.config.isRunPostProcessorLogic();
90  	}
91  
92  	public void process(String documentId, String nodeInstanceId) throws Exception {
93  		if (documentId == null) {
94  			throw new IllegalArgumentException("Cannot process a null document id.");
95  		}
96  		MDC.put("docId", documentId);
97  		boolean success = true;
98  		RouteContext context = RouteContext.createNewRouteContext();
99  		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.clearCurrentRouteContext();
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(document.getDocumentId());
444 			boolean approved = false;
445 			for (Iterator iter = requests.iterator(); iter.hasNext();) {
446 				ActionRequestValue actionRequest = (ActionRequestValue) iter.next();
447 				if (actionRequest.isApproveOrCompleteRequest() && actionRequest.isDone()) { // &&
448 																							// !(actionRequest.getRouteMethodName().equals(KewApiConstants.ADHOC_ROUTE_MODULE_NAME)
449 																							// &&
450 																							// actionRequest.isReviewerUser()
451 																							// &&
452 																							// document.getInitiatorWorkflowId().equals(actionRequest.getWorkflowId())))
453 																							// {
454 					LOG.debug("Found at least one processed approve request so document can be approved");
455 					approved = true;
456 					break;
457 				}
458 			}
459 			if (!approved) {
460 				LOG.debug("Document requires at least one request and none are present");
461 				// TODO what route method name to pass to this?
462 				throw new RouteManagerException("Document should have generated at least one approval request.");
463 			}
464 		}
465 	}
466 
467 	private DocumentRouteHeaderValue notifyPostProcessor(RouteContext context, DocumentRouteStatusChange event) {
468 		DocumentRouteHeaderValue document = context.getDocument();
469 		if (context.isSimulation()) {
470 			return document;
471 		}
472 		if (hasContactedPostProcessor(context, event)) {
473 			return document;
474 		}
475 		String documentId = event.getDocumentId();
476 		PerformanceLogger performanceLogger = new PerformanceLogger(documentId);
477 		ProcessDocReport processReport = null;
478 		PostProcessor postProc = null;
479         try {
480             // use the document's post processor unless specified by the runPostProcessorLogic not to
481             if (!isRunPostProcessorLogic()) {
482                 postProc = new DefaultPostProcessor();
483             } else {
484                 postProc = document.getDocumentType().getPostProcessor();
485             }
486         } catch (Exception e) {
487             LOG.error("Error retrieving PostProcessor for document " + document.getDocumentId(), e);
488             throw new RouteManagerException("Error retrieving PostProcessor for document " + document.getDocumentId(), e);
489         }
490 		try {
491 			processReport = postProc.doRouteStatusChange(event);
492 		} catch (Exception e) {
493 			LOG.error("Error notifying post processor", e);
494 			throw new RouteManagerException(KewApiConstants.POST_PROCESSOR_FAILURE_MESSAGE, e);
495 		} finally {
496 			performanceLogger.log("Time to notifyPostProcessor of event " + event.getDocumentEventCode() + ".");
497 		}
498 
499 		if (!processReport.isSuccess()) {
500 			LOG.warn("PostProcessor failed to process document: " + processReport.getMessage());
501 			throw new RouteManagerException(KewApiConstants.POST_PROCESSOR_FAILURE_MESSAGE + processReport.getMessage());
502 		}
503 		return document;
504 	}
505 
506 	/**
507 	 * Returns true if the post processor has already been contacted about a
508 	 * PROCESSED or FINAL post processor change. If the post processor has not
509 	 * been contacted, this method will record on the document that it has been.
510 	 *
511 	 * This is because, in certain cases, a document could end up in exception
512 	 * routing after it has already gone PROCESSED or FINAL (i.e. on Mass Action
513 	 * processing) and we don't want to re-contact the post processor in these
514 	 * cases.
515 	 */
516 	private boolean hasContactedPostProcessor(RouteContext context, DocumentRouteStatusChange event) {
517 		// get the initial node instance, the root branch is where we will store
518 		// the state
519 		Branch rootBranch = context.getDocument().getRootBranch();
520 		String key = null;
521 		if (KewApiConstants.ROUTE_HEADER_PROCESSED_CD.equals(event.getNewRouteStatus())) {
522 			key = KewApiConstants.POST_PROCESSOR_PROCESSED_KEY;
523 		} else if (KewApiConstants.ROUTE_HEADER_FINAL_CD.equals(event.getNewRouteStatus())) {
524 			key = KewApiConstants.POST_PROCESSOR_FINAL_KEY;
525 		} else {
526 			return false;
527 		}
528 		BranchState branchState = null;
529 		if (rootBranch != null) {
530 		    branchState = rootBranch.getBranchState(key);
531 		} else {
532 		    return false;
533 		}
534 		if (branchState == null) {
535 			branchState = new BranchState();
536 			branchState.setKey(key);
537 			branchState.setValue("true");
538 			rootBranch.addBranchState(branchState);
539 			saveBranch(context, rootBranch);
540 			return false;
541 		}
542 		return "true".equals(branchState.getValue());
543 	}
544 
545 	/**
546 	 * TODO in some cases, someone may modify the route header in the post
547 	 * processor, if we don't save before and reload after we will get an
548 	 * optimistic lock exception, we need to work on a better solution for this!
549 	 * TODO get the routeContext in this method - it should be a better object
550 	 * than the nodeInstance
551 	 */
552 	private DocumentRouteHeaderValue notifyPostProcessor(DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance, DocumentRouteLevelChange event) {
553 		document = getRouteHeaderService().saveRouteHeader(document);
554 		ProcessDocReport report = null;
555 		try {
556 	        PostProcessor postProcessor = null;
557 	        // use the document's post processor unless specified by the runPostProcessorLogic not to
558 	        if (!isRunPostProcessorLogic()) {
559 	            postProcessor = new DefaultPostProcessor();
560 	        } else {
561 	            postProcessor = document.getDocumentType().getPostProcessor();
562 	        }
563 			report = postProcessor.doRouteLevelChange(event);
564 		} catch (Exception e) {
565 			LOG.warn("Problems contacting PostProcessor", e);
566 			throw new RouteManagerException("Problems contacting PostProcessor:  " + e.getMessage());
567 		}
568 		document = getRouteHeaderService().getRouteHeader(document.getDocumentId());
569 		if (!report.isSuccess()) {
570 			LOG.error("PostProcessor rejected route level change::" + report.getMessage(), report.getProcessException());
571 			throw new RouteManagerException("Route Level change failed in post processor::" + report.getMessage());
572 		}
573 		return document;
574 	}
575 
576     /**
577      * TODO get the routeContext in this method - it should be a better object
578      * than the nodeInstance
579      */
580 	private DocumentRouteHeaderValue notifyPostProcessorBeforeProcess(DocumentRouteHeaderValue document, String nodeInstanceId) {
581 	    return notifyPostProcessorBeforeProcess(document, nodeInstanceId, new BeforeProcessEvent(document.getDocumentId(),document.getAppDocId(),nodeInstanceId));
582 	}
583 
584     /**
585      * TODO get the routeContext in this method - it should be a better object
586      * than the nodeInstance
587      */
588     private DocumentRouteHeaderValue notifyPostProcessorBeforeProcess(DocumentRouteHeaderValue document, String nodeInstanceId, BeforeProcessEvent event) {
589         ProcessDocReport report = null;
590         try {
591             PostProcessor postProcessor = null;
592             // use the document's post processor unless specified by the runPostProcessorLogic not to
593             if (!isRunPostProcessorLogic()) {
594                 postProcessor = new DefaultPostProcessor();
595             } else {
596                 postProcessor = document.getDocumentType().getPostProcessor();
597             }
598             report = postProcessor.beforeProcess(event);
599         } catch (Exception e) {
600             LOG.warn("Problems contacting PostProcessor", e);
601             throw new RouteManagerException("Problems contacting PostProcessor:  " + e.getMessage());
602         }
603         document = getRouteHeaderService().getRouteHeader(document.getDocumentId());
604         if (!report.isSuccess()) {
605             LOG.error("PostProcessor rejected route level change::" + report.getMessage(), report.getProcessException());
606             throw new RouteManagerException("Route Level change failed in post processor::" + report.getMessage());
607         }
608         return document;
609     }
610 
611     protected void lockAdditionalDocuments(DocumentRouteHeaderValue document) throws Exception {
612 		DocumentLockingEvent lockingEvent = new DocumentLockingEvent(document.getDocumentId(), document.getAppDocId());
613 		// TODO this shows up in a few places and could totally be extracted to a method
614 		PostProcessor postProcessor = null;
615         // use the document's post processor unless specified by the runPostProcessorLogic not to
616         if (!isRunPostProcessorLogic()) {
617             postProcessor = new DefaultPostProcessor();
618         } else {
619             postProcessor = document.getDocumentType().getPostProcessor();
620         }
621         List<String> documentIdsToLock = postProcessor.getDocumentIdsToLock(lockingEvent);
622         if (documentIdsToLock != null && !documentIdsToLock.isEmpty()) {
623         	for (String documentId : documentIdsToLock) {
624         		if ( LOG.isInfoEnabled() ) {
625     				LOG.info("Aquiring additional lock on document " + documentId);
626     			}
627         		getRouteHeaderService().lockRouteHeader(documentId);
628         		if ( LOG.isInfoEnabled() ) {
629         			LOG.info("Aquired lock on document " + documentId);
630         		}
631         	}
632         }
633 	}
634 
635     /**
636      * TODO get the routeContext in this method - it should be a better object
637      * than the nodeInstance
638      */
639     private DocumentRouteHeaderValue notifyPostProcessorAfterProcess(DocumentRouteHeaderValue document, String nodeInstanceId, boolean successfullyProcessed) {
640     	if (document == null) {
641     		// this could happen if we failed to acquire the lock on the document
642     		return null;
643     	}
644         return notifyPostProcessorAfterProcess(document, nodeInstanceId, new AfterProcessEvent(document.getDocumentId(),document.getAppDocId(),nodeInstanceId,successfullyProcessed));
645     }
646 
647     /**
648      * TODO get the routeContext in this method - it should be a better object
649      * than the nodeInstance
650      */
651     private DocumentRouteHeaderValue notifyPostProcessorAfterProcess(DocumentRouteHeaderValue document, String nodeInstanceId, AfterProcessEvent event) {
652         ProcessDocReport report = null;
653         try {
654             PostProcessor postProcessor = null;
655             // use the document's post processor unless specified by the runPostProcessorLogic not to
656             if (!isRunPostProcessorLogic()) {
657                 postProcessor = new DefaultPostProcessor();
658             } else {
659                 postProcessor = document.getDocumentType().getPostProcessor();
660             }
661             report = postProcessor.afterProcess(event);
662         } catch (Exception e) {
663             throw new RouteManagerException("Problems contacting PostProcessor.",e);
664         }
665         document = getRouteHeaderService().getRouteHeader(document.getDocumentId());
666         if (!report.isSuccess()) {
667             LOG.error("PostProcessor rejected route level change::" + report.getMessage(), report.getProcessException());
668             throw new RouteManagerException("Route Level change failed in post processor::" + report.getMessage());
669         }
670         return document;
671     }
672 
673 	/**
674 	 * This method initializes the document by materializing and activating the
675 	 * first node instance on the document.
676 	 */
677 	public void initializeDocument(DocumentRouteHeaderValue document) {
678 		// we set up a local route context here just so that we are able to
679 		// utilize the saveNode method at the end of
680 		// this method. Incidentally, this was changed from pulling the existing
681 		// context out because it would override
682 		// the document in the route context in the case of a document being
683 		// initialized for reporting purposes.
684 		RouteContext context = new RouteContext();
685 		context.setDocument(document);
686 
687 		if (context.getEngineState() == null) {
688 			context.setEngineState(new EngineState());
689 		}
690 
691 		ProcessDefinitionBo process = document.getDocumentType().getPrimaryProcess();
692 
693         if (process == null) {
694             throw new IllegalDocumentTypeException("DocumentType '" + document.getDocumentType().getName() + "' has no primary process configured!");
695         }
696 
697         if (process.getInitialRouteNode() != null) {
698             RouteNodeInstance nodeInstance = helper.getNodeFactory().createRouteNodeInstance(document.getDocumentId(), process.getInitialRouteNode());
699             nodeInstance.setActive(true);
700             helper.getNodeFactory().createBranch(KewApiConstants.PRIMARY_BRANCH_NAME, null, nodeInstance);
701             nodeInstance = saveNode(context, nodeInstance);
702             document.getInitialRouteNodeInstances().add(nodeInstance);
703         }
704 	}
705 
706     private boolean isRunawayProcessDetected(EngineState engineState) throws NumberFormatException {
707 	    String maxNodesConstant = getParameterService().getParameterValueAsString(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.MAX_NODES_BEFORE_RUNAWAY_PROCESS);
708 	    int maxNodes = StringUtils.isEmpty(maxNodesConstant) ? 50 : Integer.valueOf(maxNodesConstant);
709 	    return engineState.getCompleteNodeInstances().size() > maxNodes;
710 	}
711 
712     protected RouteNodeService getRouteNodeService() {
713 		return routeNodeService;
714 	}
715 
716 	protected RouteHeaderService getRouteHeaderService() {
717 		return routeHeaderService;
718 	}
719 
720     protected ParameterService getParameterService() {
721         if (parameterService == null) {
722             parameterService = CoreFrameworkServiceLocator.getParameterService();
723         }
724 		return parameterService;
725 	}
726 
727     public void setRouteNodeService(RouteNodeService routeNodeService) {
728         this.routeNodeService = routeNodeService;
729     }
730 
731     public void setRouteHeaderService(RouteHeaderService routeHeaderService) {
732         this.routeHeaderService = routeHeaderService;
733     }
734 
735     public void setParameterService(ParameterService parameterService) {
736         this.parameterService = parameterService;
737     }
738 }