View Javadoc

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