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