View Javadoc

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