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