View Javadoc

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