001/** 002 * Copyright 2005-2017 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.kew.engine; 017 018import org.apache.commons.lang.StringUtils; 019import org.apache.log4j.MDC; 020import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator; 021import org.kuali.rice.coreservice.framework.parameter.ParameterService; 022import org.kuali.rice.kew.actionrequest.ActionRequestValue; 023import org.kuali.rice.kew.api.KewApiConstants; 024import org.kuali.rice.kew.api.doctype.IllegalDocumentTypeException; 025import org.kuali.rice.kew.api.exception.InvalidActionTakenException; 026import org.kuali.rice.kew.api.exception.WorkflowException; 027import org.kuali.rice.kew.engine.node.Branch; 028import org.kuali.rice.kew.engine.node.BranchState; 029import org.kuali.rice.kew.engine.node.ProcessDefinitionBo; 030import org.kuali.rice.kew.engine.node.ProcessResult; 031import org.kuali.rice.kew.engine.node.RouteNodeInstance; 032import org.kuali.rice.kew.engine.node.RouteNodeUtils; 033import org.kuali.rice.kew.engine.node.service.RouteNodeService; 034import org.kuali.rice.kew.engine.transition.Transition; 035import org.kuali.rice.kew.engine.transition.TransitionEngine; 036import org.kuali.rice.kew.engine.transition.TransitionEngineFactory; 037import org.kuali.rice.kew.exception.RouteManagerException; 038import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent; 039import org.kuali.rice.kew.framework.postprocessor.BeforeProcessEvent; 040import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent; 041import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange; 042import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange; 043import org.kuali.rice.kew.framework.postprocessor.PostProcessor; 044import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport; 045import org.kuali.rice.kew.postprocessor.DefaultPostProcessor; 046import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 047import org.kuali.rice.kew.routeheader.service.RouteHeaderService; 048import org.kuali.rice.kew.service.KEWServiceLocator; 049import org.kuali.rice.kew.util.PerformanceLogger; 050import org.kuali.rice.krad.data.KradDataServiceLocator; 051import org.kuali.rice.krad.util.KRADConstants; 052 053import java.sql.Timestamp; 054import java.util.ArrayList; 055import java.util.Collection; 056import java.util.Iterator; 057import java.util.LinkedList; 058import java.util.List; 059 060 061/** 062 * The standard and supported implementation of the WorkflowEngine. Runs a processing loop against a given 063 * Document, processing nodes on the document until the document is completed or a node halts the 064 * processing. 065 * 066 * @author Kuali Rice Team (rice.collab@kuali.org) 067 */ 068public class StandardWorkflowEngine implements WorkflowEngine { 069 070 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(StandardWorkflowEngine.class); 071 072 protected final RouteHelper helper = new RouteHelper(); 073 protected RouteNodeService routeNodeService; 074 protected RouteHeaderService routeHeaderService; 075 protected ParameterService parameterService; 076 protected OrchestrationConfig config; 077 078 public StandardWorkflowEngine() {} 079 080 protected StandardWorkflowEngine(RouteNodeService routeNodeService, RouteHeaderService routeHeaderService, 081 ParameterService parameterService, OrchestrationConfig config) { 082 this.routeNodeService = routeNodeService; 083 this.routeHeaderService = routeHeaderService; 084 this.parameterService = parameterService; 085 this.config = config; 086 } 087 088 public boolean isRunPostProcessorLogic() { 089 return this.config.isRunPostProcessorLogic(); 090 } 091 092 public void process(String documentId, String nodeInstanceId) throws Exception { 093 if (documentId == null) { 094 throw new IllegalArgumentException("Cannot process a null document id."); 095 } 096 MDC.put("docId", documentId); 097 boolean success = true; 098 RouteContext context = RouteContext.createNewRouteContext(); 099 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.releaseCurrentRouteContext(); 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}