View Javadoc

1   /*
2    * Copyright 2005-2007 The Kuali Foundation
3    *
4    *
5    * Licensed under the Educational Community License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.opensource.org/licenses/ecl2.php
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.kuali.rice.kew.engine;
18  
19  import org.apache.log4j.MDC;
20  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
21  import org.kuali.rice.kew.engine.node.*;
22  import org.kuali.rice.kew.engine.node.Process;
23  import org.kuali.rice.kew.engine.node.service.RouteNodeService;
24  import org.kuali.rice.kew.engine.transition.Transition;
25  import org.kuali.rice.kew.engine.transition.TransitionEngine;
26  import org.kuali.rice.kew.engine.transition.TransitionEngineFactory;
27  import org.kuali.rice.kew.exception.InvalidActionTakenException;
28  import org.kuali.rice.kew.exception.RouteManagerException;
29  import org.kuali.rice.kew.exception.WorkflowException;
30  import org.kuali.rice.kew.postprocessor.*;
31  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
32  import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
33  import org.kuali.rice.kew.service.KEWServiceLocator;
34  import org.kuali.rice.kew.util.KEWConstants;
35  import org.kuali.rice.kew.util.PerformanceLogger;
36  import org.kuali.rice.kew.util.Utilities;
37  import org.kuali.rice.kns.util.KNSConstants;
38  
39  import java.util.*;
40  
41  
42  /**
43   * The standard and supported implementation of the WorkflowEngine.  Runs a processing loop against a given
44   * Document, processing nodes on the document until the document is completed or a node halts the
45   * processing.
46   *
47   * @author Kuali Rice Team (rice.collab@kuali.org)
48   */
49  public class StandardWorkflowEngine implements WorkflowEngine {
50  
51  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardWorkflowEngine.class);
52  
53  	protected RouteHelper helper = new RouteHelper();
54  	private boolean runPostProcessorLogic = true;
55  
56      public StandardWorkflowEngine() {}
57  
58  	public StandardWorkflowEngine(boolean runPostProcessorLogic) {
59  	    setRunPostProcessorLogic(runPostProcessorLogic);
60  	}
61  
62  	public void setRunPostProcessorLogic(boolean runPostProcessorLogic) {
63  	    this.runPostProcessorLogic = runPostProcessorLogic;
64  	}
65  
66  	public boolean isRunPostProcessorLogic() {
67  	    return this.runPostProcessorLogic;
68  	}
69  
70  	public void process(Long documentId, Long nodeInstanceId) throws Exception {
71  		if (documentId == null) {
72  			throw new IllegalArgumentException("Cannot process a null document id.");
73  		}
74  		MDC.put("docId", documentId);
75  		boolean success = true;
76  		RouteContext context = RouteContext.createNewRouteContext();
77  		try {
78  			if ( LOG.isInfoEnabled() ) {
79  				LOG.info("Aquiring lock on document " + documentId);
80  			}
81  			KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId, true);
82  			if ( LOG.isInfoEnabled() ) {
83  				LOG.info("Aquired lock on document " + documentId);
84  			}
85  
86  			DocumentRouteHeaderValue document = getRouteHeaderService().getRouteHeader(documentId);
87  			context.setDocument(document);
88  			lockAdditionalDocuments(document);
89  
90  			if ( LOG.isInfoEnabled() ) {
91  				LOG.info("Processing document: " + documentId + " : " + nodeInstanceId);
92  			}
93  
94  			try {
95  	            document = notifyPostProcessorBeforeProcess(document, nodeInstanceId);
96  	            context.setDocument(document);
97              } catch (Exception e) {
98                  LOG.warn("Problems contacting PostProcessor before engine process", e);
99                  throw new RouteManagerException("Problems contacting PostProcessor:  " + e.getMessage());
100             }
101             if (!document.isRoutable()) {
102 				LOG.debug("Document not routable so returning with doing no action");
103 				return;
104 			}
105 			List nodeInstancesToProcess = new LinkedList();
106 			if (nodeInstanceId == null) {
107 				// pulls the node instances from the passed in document
108 				nodeInstancesToProcess.addAll(RouteNodeUtils.getActiveNodeInstances(document));
109 			} else {
110 				RouteNodeInstance instanceNode = RouteNodeUtils.findRouteNodeInstanceById(nodeInstanceId,document);
111 				if (instanceNode == null) {
112 					throw new IllegalArgumentException("Invalid node instance id: " + nodeInstanceId);
113 				}
114 				nodeInstancesToProcess.add(instanceNode);
115 			}
116 
117 			context.setEngineState(new EngineState());
118 			ProcessContext processContext = new ProcessContext(true, nodeInstancesToProcess);
119 			try {
120 				while (!nodeInstancesToProcess.isEmpty()) {
121 					context.setNodeInstance((RouteNodeInstance) nodeInstancesToProcess.remove(0));
122 					processContext = processNodeInstance(context, helper);
123 					if (processContext.isComplete() && !processContext.getNextNodeInstances().isEmpty()) {
124 						nodeInstancesToProcess.addAll(processContext.getNextNodeInstances());
125 					}
126 				}
127 				context.setDocument(nodePostProcess(context));
128 			} catch (Exception e) {
129 				success = false;
130 				// TODO throw a new 'RoutingException' which holds the
131 				// RoutingState
132 				throw new RouteManagerException(e, context);
133 			}
134 		} finally {
135 			if ( LOG.isInfoEnabled() ) {
136 				LOG.info((success ? "Successfully processed" : "Failed to process") + " document: " + documentId + " : " + nodeInstanceId);
137 			}
138 			try {
139 	            notifyPostProcessorAfterProcess(context.getDocument(), nodeInstanceId, success);
140             } catch (Exception e) {
141                 LOG.warn("Problems contacting PostProcessor after engine process", e);
142                 throw new RouteManagerException("Problems contacting PostProcessor:  " + e.getMessage(), context);
143             }
144 			RouteContext.clearCurrentRouteContext();
145 			MDC.remove("docId");
146 		}
147 	}
148 
149 	protected ProcessContext processNodeInstance(RouteContext context, RouteHelper helper) throws Exception {
150 		RouteNodeInstance nodeInstance = context.getNodeInstance();
151 		if ( LOG.isDebugEnabled() ) {
152 			LOG.debug("Processing node instance: " + nodeInstance.getRouteNode().getRouteNodeName());
153 		}
154 		if (checkAssertions(context)) {
155 			// returning an empty context causes the outer loop to terminate
156 			return new ProcessContext();
157 		}
158 		TransitionEngine transitionEngine = TransitionEngineFactory.createTransitionEngine(nodeInstance);
159 		ProcessResult processResult = transitionEngine.isComplete(context);
160 		nodeInstance.setInitial(false);
161 
162 		// if this nodeInstance already has next node instance we don't need to
163 		// go to the TE
164 		if (processResult.isComplete()) {
165 			if ( LOG.isDebugEnabled() ) {
166 				LOG.debug("Routing node has completed: " + nodeInstance.getRouteNode().getRouteNodeName());
167 			}
168 
169 			context.getEngineState().getCompleteNodeInstances().add(nodeInstance.getRouteNodeInstanceId());
170 			List nextNodeCandidates = invokeTransition(context, context.getNodeInstance(), processResult, transitionEngine);
171 
172 			// iterate over the next node candidates sending them through the
173 			// transition engine's transitionTo method
174 			// one at a time for a potential switch. Place the transition
175 			// engines result back in the 'actual' next node
176 			// list which we put in the next node before doing work.
177 			List<RouteNodeInstance> nodesToActivate = new ArrayList<RouteNodeInstance>();
178 			if (!nextNodeCandidates.isEmpty()) {
179 				// KULRICE-4274: Hierarchy Routing Node issues
180 				// No longer change nextNodeInstances in place, instead we create a local and assign our local list below
181 				// the loop so the post processor doesn't save a RouteNodeInstance in an intermediate state
182 				ArrayList<RouteNodeInstance> nextNodeInstances = new ArrayList<RouteNodeInstance>();
183 
184 				for (Iterator nextIt = nextNodeCandidates.iterator(); nextIt.hasNext();) {
185 					RouteNodeInstance nextNodeInstance = (RouteNodeInstance) nextIt.next();
186 					transitionEngine = TransitionEngineFactory.createTransitionEngine(nextNodeInstance);
187 					RouteNodeInstance currentNextNodeInstance = nextNodeInstance;
188 					nextNodeInstance = transitionEngine.transitionTo(nextNodeInstance, context);
189 					// if the next node has changed, we need to remove our
190 					// current node as a next node of the original node
191 					if (!currentNextNodeInstance.equals(nextNodeInstance)) {
192 						currentNextNodeInstance.getPreviousNodeInstances().remove(nodeInstance);
193 					}
194 					// before adding next node instance, be sure that it's not
195 					// already linked via previous node instances
196 					// this is to prevent the engine from setting up references
197 					// on nodes that already reference each other.
198 					// the primary case being when we are walking over an
199 					// already constructed graph of nodes returned from a
200 					// dynamic node - probably a more sensible approach would be
201 					// to check for the existence of the link and moving on
202 					// if it's been established.
203 					nextNodeInstance.getPreviousNodeInstances().remove(nodeInstance);
204 					nextNodeInstances.add(nextNodeInstance);
205 					handleBackwardCompatibility(context, nextNodeInstance);
206 					// call the post processor
207 					notifyNodeChange(context, nextNodeInstance);
208 					nodesToActivate.add(nextNodeInstance);
209  					// TODO update document content on context?
210  				}
211  				// assign our local list here so the post processor doesn't save a RouteNodeInstance in an intermediate state
212 				for (RouteNodeInstance nextNodeInstance : nextNodeInstances) {
213 					nodeInstance.addNextNodeInstance(nextNodeInstance);
214 				}
215  			}
216  
217  			// deactive the current active node
218 			nodeInstance.setComplete(true);
219 			nodeInstance.setActive(false);
220 			// active the nodes we're transitioning into
221 			for (RouteNodeInstance nodeToActivate : nodesToActivate) {
222 				nodeToActivate.setActive(true);
223 			}
224 		} else {
225 		    nodeInstance.setComplete(false);
226         }
227 
228 		saveNode(context, nodeInstance);
229 		return new ProcessContext(nodeInstance.isComplete(), nodeInstance.getNextNodeInstances());
230 	}
231 
232 	/**
233 	 * Checks various assertions regarding the processing of the current node.
234 	 * If this method returns true, then the node will not be processed.
235 	 *
236 	 * This method will throw an exception if it deems that the processing is in
237 	 * a illegal state.
238 	 */
239 	private boolean checkAssertions(RouteContext context) throws Exception {
240 		if (context.getNodeInstance().isComplete()) {
241 			if ( LOG.isDebugEnabled() ) {
242 				LOG.debug("The node has already been completed: " + context.getNodeInstance().getRouteNode().getRouteNodeName());
243 			}
244 			return true;
245 		}
246 		if (isRunawayProcessDetected(context.getEngineState())) {
247 //			 TODO more info in message
248 			throw new WorkflowException("Detected runaway process.");
249 		}
250 		return false;
251 	}
252 
253 	/**
254 	 * Invokes the transition and returns the next node instances to transition
255 	 * to from the current node instance on the route context.
256 	 *
257 	 * This is a 3-step process:
258 	 *
259 	 * <pre>
260 	 *  1) If the node instance already has next nodes, return those,
261 	 *  2) otherwise, invoke the transition engine for the node, if the resulting node instances are not empty, return those,
262 	 *  3) lastly, if our node is in a process and no next nodes were returned from it's transition engine, invoke the
263 	 *     transition engine of the process node and return the resulting node instances.
264 	 * </pre>
265 	 */
266 	/*
267 	 * private List invokeTransition(RouteContext context, RouteNodeInstance
268 	 * nodeInstance, ProcessResult processResult, TransitionEngine
269 	 * transitionEngine) throws Exception { List nextNodeInstances =
270 	 * nodeInstance.getNextNodeInstances(); if (nextNodeInstances.isEmpty()) {
271 	 * Transition result = transitionEngine.transitionFrom(context,
272 	 * processResult); nextNodeInstances = result.getNextNodeInstances(); if
273 	 * (nextNodeInstances.isEmpty() && nodeInstance.isInProcess()) {
274 	 * transitionEngine =
275 	 * TransitionEngineFactory.createTransitionEngine(nodeInstance.getProcess());
276 	 * nextNodeInstances = invokeTransition(context, nodeInstance.getProcess(),
277 	 * processResult, transitionEngine); } } return nextNodeInstances; }
278 	 */
279 
280 	private List invokeTransition(RouteContext context, RouteNodeInstance nodeInstance, ProcessResult processResult, TransitionEngine transitionEngine) throws Exception {
281 		List nextNodeInstances = nodeInstance.getNextNodeInstances();
282 		if (nextNodeInstances.isEmpty()) {
283 			Transition result = transitionEngine.transitionFrom(context, processResult);
284 			nextNodeInstances = result.getNextNodeInstances();
285 			if (nextNodeInstances.isEmpty() && nodeInstance.isInProcess()) {
286 				transitionEngine = TransitionEngineFactory.createTransitionEngine(nodeInstance.getProcess());
287 				context.setNodeInstance(nodeInstance);
288 				nextNodeInstances = invokeTransition(context, nodeInstance.getProcess(), processResult, transitionEngine);
289 			}
290 		}
291 		return nextNodeInstances;
292 	}
293 
294 	/*
295 	 * private List invokeTransition(RouteContext context, RouteNodeInstance
296 	 * process, ProcessResult processResult) throws Exception {
297 	 * RouteNodeInstance nodeInstance = (context.getNodeInstance() ; List
298 	 * nextNodeInstances = nodeInstance.getNextNodeInstances(); if
299 	 * (nextNodeInstances.isEmpty()) { TransitionEngine transitionEngine =
300 	 * TransitionEngineFactory.createTransitionEngine(nodeInstance); Transition
301 	 * result = transitionEngine.transitionFrom(context, processResult);
302 	 * nextNodeInstances = result.getNextNodeInstances(); if
303 	 * (nextNodeInstances.isEmpty() && nodeInstance.isInProcess()) {
304 	 * transitionEngine =
305 	 * TransitionEngineFactory.createTransitionEngine(nodeInstance.getProcess());
306 	 * nextNodeInstances = invokeTransition(context, nodeInstance.getProcess(),
307 	 * processResult, transitionEngine); } } return nextNodeInstances; }
308 	 *
309 	 */private void notifyNodeChange(RouteContext context, RouteNodeInstance nextNodeInstance) throws Exception {
310 		if (!context.isSimulation()) {
311 			RouteNodeInstance nodeInstance = context.getNodeInstance();
312 			// if application document status transition has been defined, update the status
313 			String nextStatus = nodeInstance.getRouteNode().getNextDocStatus();
314 			if (nextStatus != null && nextStatus.length() > 0){
315 				context.getDocument().updateAppDocStatus(nextStatus);
316 			}
317 
318 			DocumentRouteLevelChange event = new DocumentRouteLevelChange(context.getDocument().getRouteHeaderId(), context.getDocument().getAppDocId(), CompatUtils.getLevelForNode(context.getDocument().getDocumentType(), context.getNodeInstance()
319 					.getRouteNode().getRouteNodeName()), CompatUtils.getLevelForNode(context.getDocument().getDocumentType(), nextNodeInstance.getRouteNode().getRouteNodeName()), nodeInstance.getRouteNode().getRouteNodeName(), nextNodeInstance
320 					.getRouteNode().getRouteNodeName(), nodeInstance.getRouteNodeInstanceId(), nextNodeInstance.getRouteNodeInstanceId());
321 			context.setDocument(notifyPostProcessor(context.getDocument(), nodeInstance, event));
322 		}
323 	}
324 
325 	private void handleBackwardCompatibility(RouteContext context, RouteNodeInstance nextNodeInstance) {
326 		context.getDocument().setDocRouteLevel(new Integer(context.getDocument().getDocRouteLevel().intValue() + 1)); // preserve
327 																														// route
328 																														// level
329 																														// concept
330 																														// if
331 																														// possible
332 		saveDocument(context);
333 	}
334 
335 	private void saveDocument(RouteContext context) {
336 		if (!context.isSimulation()) {
337 			getRouteHeaderService().saveRouteHeader(context.getDocument());
338 		}
339 	}
340 
341 	private void saveBranch(RouteContext context, Branch branch) {
342 		if (!context.isSimulation()) {
343 			KEWServiceLocator.getRouteNodeService().save(branch);
344 		}
345 	}
346 
347 	protected void saveNode(RouteContext context, RouteNodeInstance nodeInstance) {
348 		if (!context.isSimulation()) {
349 			getRouteNodeService().save(nodeInstance);
350 		} else {
351 			// if we are in simulation mode, lets go ahead and assign some id
352 			// values to our beans
353 			for (Iterator<RouteNodeInstance> iterator = nodeInstance.getNextNodeInstances().iterator(); iterator.hasNext();) {
354 				RouteNodeInstance routeNodeInstance = (RouteNodeInstance) iterator.next();
355 				if (routeNodeInstance.getRouteNodeInstanceId() == null) {
356 					routeNodeInstance.setRouteNodeInstanceId(context.getEngineState().getNextSimulationId());
357 				}
358 			}
359 			if (nodeInstance.getProcess() != null && nodeInstance.getProcess().getRouteNodeInstanceId() == null) {
360 				nodeInstance.getProcess().setRouteNodeInstanceId(context.getEngineState().getNextSimulationId());
361 			}
362 			if (nodeInstance.getBranch() != null && nodeInstance.getBranch().getBranchId() == null) {
363 				nodeInstance.getBranch().setBranchId(context.getEngineState().getNextSimulationId());
364 			}
365 		}
366 	}
367 
368 	// TODO extract this into some sort of component which handles transitioning
369 	// document state
370 	protected DocumentRouteHeaderValue nodePostProcess(RouteContext context) throws InvalidActionTakenException {
371 		DocumentRouteHeaderValue document = context.getDocument();
372 		Collection<RouteNodeInstance> activeNodes = getRouteNodeService().getActiveNodeInstances(document.getRouteHeaderId());
373 		boolean moreNodes = false;
374 		for (Iterator<RouteNodeInstance> iterator = activeNodes.iterator(); iterator.hasNext();) {
375 			RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator.next();
376 			moreNodes = moreNodes || !nodeInstance.isComplete();
377 		}
378 		List pendingRequests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getRouteHeaderId());
379 		boolean activeApproveRequests = false;
380 		boolean activeAckRequests = false;
381 		for (Iterator iterator = pendingRequests.iterator(); iterator.hasNext();) {
382 			ActionRequestValue request = (ActionRequestValue) iterator.next();
383 			activeApproveRequests = request.isApproveOrCompleteRequest() || activeApproveRequests;
384 			activeAckRequests = request.isAcknowledgeRequest() || activeAckRequests;
385 		}
386 		// TODO is the logic for going processed still going to be valid?
387 		if (!document.isProcessed() && (!moreNodes || !activeApproveRequests)) {
388 			if ( LOG.isDebugEnabled() ) {
389 				LOG.debug("No more nodes for this document " + document.getRouteHeaderId());
390 			}
391 			// TODO perhaps the policies could also be factored out?
392 			checkDefaultApprovalPolicy(document);
393 			LOG.debug("Marking document approved");
394 			// TODO factor out this magical post processing
395 			DocumentRouteStatusChange event = new DocumentRouteStatusChange(document.getRouteHeaderId(), document.getAppDocId(), document.getDocRouteStatus(), KEWConstants.ROUTE_HEADER_APPROVED_CD);
396 			document.markDocumentApproved();
397 			// saveDocument(context);
398 			notifyPostProcessor(context, event);
399 
400 			LOG.debug("Marking document processed");
401 			event = new DocumentRouteStatusChange(document.getRouteHeaderId(), document.getAppDocId(), document.getDocRouteStatus(), KEWConstants.ROUTE_HEADER_PROCESSED_CD);
402 			document.markDocumentProcessed();
403 			// saveDocument(context);
404 			notifyPostProcessor(context, event);
405 		}
406 
407 		// if document is processed and no pending action requests put the
408 		// document into the finalized state.
409 		if (document.isProcessed()) {
410 			DocumentRouteStatusChange event = new DocumentRouteStatusChange(document.getRouteHeaderId(), document.getAppDocId(), document.getDocRouteStatus(), KEWConstants.ROUTE_HEADER_FINAL_CD);
411 			List actionRequests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getRouteHeaderId());
412 			if (actionRequests.isEmpty()) {
413 				document.markDocumentFinalized();
414 				// saveDocument(context);
415 				notifyPostProcessor(context, event);
416 			} else {
417 				boolean markFinalized = true;
418 				for (Iterator iter = actionRequests.iterator(); iter.hasNext();) {
419 					ActionRequestValue actionRequest = (ActionRequestValue) iter.next();
420 					if (KEWConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ.equals(actionRequest.getActionRequested())) {
421 						markFinalized = false;
422 					}
423 				}
424 				if (markFinalized) {
425 					document.markDocumentFinalized();
426 					// saveDocument(context);
427 					this.notifyPostProcessor(context, event);
428 				}
429 			}
430 		}
431 		saveDocument(context);
432 		return document;
433 	}
434 
435 	/**
436 	 * Check the default approval policy for the document. If the default
437 	 * approval policy is no and no approval action requests have been created
438 	 * then throw an execption so that the document will get thrown into
439 	 * exception routing.
440 	 *
441 	 * @param rh
442 	 *            route header to be checked
443 	 * @param docType
444 	 *            docType of the routeHeader to be checked.
445 	 * @throws RouteManagerException
446 	 */
447 	private void checkDefaultApprovalPolicy(DocumentRouteHeaderValue document) throws RouteManagerException {
448 		if (!document.getDocumentType().getDefaultApprovePolicy().getPolicyValue().booleanValue()) {
449 			LOG.debug("Checking if any requests have been generated for the document");
450 			List requests = KEWServiceLocator.getActionRequestService().findAllActionRequestsByRouteHeaderId(document.getRouteHeaderId());
451 			boolean approved = false;
452 			for (Iterator iter = requests.iterator(); iter.hasNext();) {
453 				ActionRequestValue actionRequest = (ActionRequestValue) iter.next();
454 				if (actionRequest.isApproveOrCompleteRequest() && actionRequest.isDone()) { // &&
455 																							// !(actionRequest.getRouteMethodName().equals(KEWConstants.ADHOC_ROUTE_MODULE_NAME)
456 																							// &&
457 																							// actionRequest.isReviewerUser()
458 																							// &&
459 																							// document.getInitiatorWorkflowId().equals(actionRequest.getWorkflowId())))
460 																							// {
461 					LOG.debug("Found at least one processed approve request so document can be approved");
462 					approved = true;
463 					break;
464 				}
465 			}
466 			if (!approved) {
467 				LOG.debug("Document requires at least one request and none are present");
468 				// TODO what route method name to pass to this?
469 				throw new RouteManagerException("Document should have generated at least one approval request.");
470 			}
471 		}
472 	}
473 
474 	private DocumentRouteHeaderValue notifyPostProcessor(RouteContext context, DocumentRouteStatusChange event) {
475 		DocumentRouteHeaderValue document = context.getDocument();
476 		if (context.isSimulation()) {
477 			return document;
478 		}
479 		if (hasContactedPostProcessor(context, event)) {
480 			return document;
481 		}
482 		Long routeHeaderId = event.getRouteHeaderId();
483 		PerformanceLogger performanceLogger = new PerformanceLogger(routeHeaderId);
484 		ProcessDocReport processReport = null;
485 		PostProcessor postProc = null;
486         try {
487             // use the document's post processor unless specified by the runPostProcessorLogic not to
488             if (!isRunPostProcessorLogic()) {
489                 postProc = new DefaultPostProcessor();
490             } else {
491                 postProc = document.getDocumentType().getPostProcessor();
492             }
493         } catch (Exception e) {
494             LOG.error("Error retrieving PostProcessor for document " + document.getRouteHeaderId(), e);
495             throw new RouteManagerException("Error retrieving PostProcessor for document " + document.getRouteHeaderId(), e);
496         }
497 		try {
498 			processReport = postProc.doRouteStatusChange(event);
499 		} catch (Exception e) {
500 			LOG.error("Error notifying post processor", e);
501 			throw new RouteManagerException(KEWConstants.POST_PROCESSOR_FAILURE_MESSAGE, e);
502 		} finally {
503 			performanceLogger.log("Time to notifyPostProcessor of event " + event.getDocumentEventCode() + ".");
504 		}
505 
506 		if (!processReport.isSuccess()) {
507 			LOG.warn("PostProcessor failed to process document: " + processReport.getMessage());
508 			throw new RouteManagerException(KEWConstants.POST_PROCESSOR_FAILURE_MESSAGE + processReport.getMessage());
509 		}
510 		return document;
511 	}
512 
513 	/**
514 	 * Returns true if the post processor has already been contacted about a
515 	 * PROCESSED or FINAL post processor change. If the post processor has not
516 	 * been contacted, this method will record on the document that it has been.
517 	 *
518 	 * This is because, in certain cases, a document could end up in exception
519 	 * routing after it has already gone PROCESSED or FINAL (i.e. on Mass Action
520 	 * processing) and we don't want to re-contact the post processor in these
521 	 * cases.
522 	 */
523 	private boolean hasContactedPostProcessor(RouteContext context, DocumentRouteStatusChange event) {
524 		// get the initial node instance, the root branch is where we will store
525 		// the state
526 		Branch rootBranch = context.getDocument().getRootBranch();
527 		String key = null;
528 		if (KEWConstants.ROUTE_HEADER_PROCESSED_CD.equals(event.getNewRouteStatus())) {
529 			key = KEWConstants.POST_PROCESSOR_PROCESSED_KEY;
530 		} else if (KEWConstants.ROUTE_HEADER_FINAL_CD.equals(event.getNewRouteStatus())) {
531 			key = KEWConstants.POST_PROCESSOR_FINAL_KEY;
532 		} else {
533 			return false;
534 		}
535 		BranchState branchState = null;
536 		if (rootBranch != null) {
537 		    branchState = rootBranch.getBranchState(key);
538 		} else {
539 		    return false;
540 		}
541 		if (branchState == null) {
542 			branchState = new BranchState(key, "true");
543 			rootBranch.addBranchState(branchState);
544 			saveBranch(context, rootBranch);
545 			return false;
546 		}
547 		return "true".equals(branchState.getValue());
548 	}
549 
550 	/**
551 	 * TODO in some cases, someone may modify the route header in the post
552 	 * processor, if we don't save before and reload after we will get an
553 	 * optimistic lock exception, we need to work on a better solution for this!
554 	 * TODO get the routeContext in this method - it should be a better object
555 	 * than the nodeInstance
556 	 */
557 	private DocumentRouteHeaderValue notifyPostProcessor(DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance, DocumentRouteLevelChange event) {
558 		getRouteHeaderService().saveRouteHeader(document);
559 		ProcessDocReport report = null;
560 		try {
561 	        PostProcessor postProcessor = null;
562 	        // use the document's post processor unless specified by the runPostProcessorLogic not to
563 	        if (!isRunPostProcessorLogic()) {
564 	            postProcessor = new DefaultPostProcessor();
565 	        } else {
566 	            postProcessor = document.getDocumentType().getPostProcessor();
567 	        }
568 			report = postProcessor.doRouteLevelChange(event);
569 		} catch (Exception e) {
570 			LOG.warn("Problems contacting PostProcessor", e);
571 			throw new RouteManagerException("Problems contacting PostProcessor:  " + e.getMessage());
572 		}
573 		document = getRouteHeaderService().getRouteHeader(document.getRouteHeaderId());
574 		if (!report.isSuccess()) {
575 			LOG.error("PostProcessor rejected route level change::" + report.getMessage(), report.getProcessException());
576 			throw new RouteManagerException("Route Level change failed in post processor::" + report.getMessage());
577 		}
578 		return document;
579 	}
580 
581     /**
582      * TODO get the routeContext in this method - it should be a better object
583      * than the nodeInstance
584      */
585 	private DocumentRouteHeaderValue notifyPostProcessorBeforeProcess(DocumentRouteHeaderValue document, Long nodeInstanceId) {
586 	    return notifyPostProcessorBeforeProcess(document, nodeInstanceId, new BeforeProcessEvent(document.getRouteHeaderId(),document.getAppDocId(),nodeInstanceId));
587 	}
588 
589     /**
590      * TODO get the routeContext in this method - it should be a better object
591      * than the nodeInstance
592      */
593     private DocumentRouteHeaderValue notifyPostProcessorBeforeProcess(DocumentRouteHeaderValue document, Long nodeInstanceId, BeforeProcessEvent event) {
594         ProcessDocReport report = null;
595         try {
596             PostProcessor postProcessor = null;
597             // use the document's post processor unless specified by the runPostProcessorLogic not to
598             if (!isRunPostProcessorLogic()) {
599                 postProcessor = new DefaultPostProcessor();
600             } else {
601                 postProcessor = document.getDocumentType().getPostProcessor();
602             }
603             report = postProcessor.beforeProcess(event);
604         } catch (Exception e) {
605             LOG.warn("Problems contacting PostProcessor", e);
606             throw new RouteManagerException("Problems contacting PostProcessor:  " + e.getMessage());
607         }
608         document = getRouteHeaderService().getRouteHeader(document.getRouteHeaderId());
609         if (!report.isSuccess()) {
610             LOG.error("PostProcessor rejected route level change::" + report.getMessage(), report.getProcessException());
611             throw new RouteManagerException("Route Level change failed in post processor::" + report.getMessage());
612         }
613         return document;
614     }
615 
616     protected void lockAdditionalDocuments(DocumentRouteHeaderValue document) throws Exception {
617 		DocumentLockingEvent lockingEvent = new DocumentLockingEvent(document.getRouteHeaderId(), document.getAppDocId());
618 		// TODO this shows up in a few places and could totally be extracted to a method
619 		PostProcessor postProcessor = null;
620         // use the document's post processor unless specified by the runPostProcessorLogic not to
621         if (!isRunPostProcessorLogic()) {
622             postProcessor = new DefaultPostProcessor();
623         } else {
624             postProcessor = document.getDocumentType().getPostProcessor();
625         }
626         List<Long> documentIdsToLock = postProcessor.getDocumentIdsToLock(lockingEvent);
627         if (documentIdsToLock != null && !documentIdsToLock.isEmpty()) {
628         	for (Long documentId : documentIdsToLock) {
629         		if ( LOG.isInfoEnabled() ) {
630     				LOG.info("Aquiring additional lock on document " + documentId);
631     			}
632         		getRouteHeaderService().lockRouteHeader(documentId, true);
633         		if ( LOG.isInfoEnabled() ) {
634         			LOG.info("Aquired lock on document " + documentId);
635         		}
636         	}
637         }
638 	}
639 
640     /**
641      * TODO get the routeContext in this method - it should be a better object
642      * than the nodeInstance
643      */
644     private DocumentRouteHeaderValue notifyPostProcessorAfterProcess(DocumentRouteHeaderValue document, Long nodeInstanceId, boolean successfullyProcessed) {
645     	if (document == null) {
646     		// this could happen if we failed to acquire the lock on the document
647     		return null;
648     	}
649         return notifyPostProcessorAfterProcess(document, nodeInstanceId, new AfterProcessEvent(document.getRouteHeaderId(),document.getAppDocId(),nodeInstanceId,successfullyProcessed));
650     }
651 
652     /**
653      * TODO get the routeContext in this method - it should be a better object
654      * than the nodeInstance
655      */
656     private DocumentRouteHeaderValue notifyPostProcessorAfterProcess(DocumentRouteHeaderValue document, Long nodeInstanceId, AfterProcessEvent event) {
657         ProcessDocReport report = null;
658         try {
659             PostProcessor postProcessor = null;
660             // use the document's post processor unless specified by the runPostProcessorLogic not to
661             if (!isRunPostProcessorLogic()) {
662                 postProcessor = new DefaultPostProcessor();
663             } else {
664                 postProcessor = document.getDocumentType().getPostProcessor();
665             }
666             report = postProcessor.afterProcess(event);
667         } catch (Exception e) {
668             LOG.warn("Problems contacting PostProcessor", e);
669             throw new RouteManagerException("Problems contacting PostProcessor:  " + e.getMessage());
670         }
671         document = getRouteHeaderService().getRouteHeader(document.getRouteHeaderId());
672         if (!report.isSuccess()) {
673             LOG.error("PostProcessor rejected route level change::" + report.getMessage(), report.getProcessException());
674             throw new RouteManagerException("Route Level change failed in post processor::" + report.getMessage());
675         }
676         return document;
677     }
678 
679 	/**
680 	 * This method initializes the document by materializing and activating the
681 	 * first node instance on the document.
682 	 */
683 	public void initializeDocument(DocumentRouteHeaderValue document) {
684 		// we set up a local route context here just so that we are able to
685 		// utilize the saveNode method at the end of
686 		// this method. Incidentally, this was changed from pulling the existing
687 		// context out because it would override
688 		// the document in the route context in the case of a document being
689 		// initialized for reporting purposes.
690 		RouteContext context = new RouteContext();
691 		context.setDocument(document);
692 		if (context.getEngineState() == null) {
693 			context.setEngineState(new EngineState());
694 		}
695 		Process process = document.getDocumentType().getPrimaryProcess();
696 		if (process == null || process.getInitialRouteNode() == null) {
697 		    if (process == null) {
698 		        throw new IllegalArgumentException("DocumentType '" + document.getDocumentType().getName() + "' has no primary process configured!");
699 		    }
700 			return;
701 		}
702 		RouteNodeInstance nodeInstance = helper.getNodeFactory().createRouteNodeInstance(document.getRouteHeaderId(), process.getInitialRouteNode());
703 		nodeInstance.setActive(true);
704 		helper.getNodeFactory().createBranch(KEWConstants.PRIMARY_BRANCH_NAME, null, nodeInstance);
705 		document.getInitialRouteNodeInstances().add(nodeInstance);
706 		saveNode(context, nodeInstance);
707 	}
708 
709 	protected RouteNodeService getRouteNodeService() {
710 		return KEWServiceLocator.getRouteNodeService();
711 	}
712 
713 	private boolean isRunawayProcessDetected(EngineState engineState) throws NumberFormatException {
714 	    String maxNodesConstant = Utilities.getKNSParameterValue(KEWConstants.KEW_NAMESPACE, KNSConstants.DetailTypes.ALL_DETAIL_TYPE, KEWConstants.MAX_NODES_BEFORE_RUNAWAY_PROCESS);
715 	    int maxNodes = (Utilities.isEmpty(maxNodesConstant)) ? 50 : Integer.valueOf(maxNodesConstant);
716 	    return engineState.getCompleteNodeInstances().size() > maxNodes;
717 	}
718 
719 	protected RouteHeaderService getRouteHeaderService() {
720 		return KEWServiceLocator.getRouteHeaderService();
721 	}
722 
723 }