001 /** 002 * Copyright 2005-2011 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 */ 016 package org.kuali.rice.kew.engine; 017 018 import org.apache.log4j.MDC; 019 import org.kuali.rice.coreservice.framework.parameter.ParameterService; 020 import org.kuali.rice.kew.actionrequest.ActionRequestFactory; 021 import org.kuali.rice.kew.actionrequest.ActionRequestValue; 022 import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient; 023 import org.kuali.rice.kew.actionrequest.service.ActionRequestService; 024 import org.kuali.rice.kew.actions.NotificationContext; 025 import org.kuali.rice.kew.actiontaken.ActionTakenValue; 026 import org.kuali.rice.kew.api.WorkflowRuntimeException; 027 import org.kuali.rice.kew.api.exception.InvalidActionTakenException; 028 import org.kuali.rice.kew.api.exception.WorkflowException; 029 import org.kuali.rice.kew.engine.node.ProcessDefinitionBo; 030 import org.kuali.rice.kew.engine.node.RouteNode; 031 import org.kuali.rice.kew.engine.node.RouteNodeInstance; 032 import org.kuali.rice.kew.engine.node.service.RouteNodeService; 033 import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 034 import org.kuali.rice.kew.routeheader.service.RouteHeaderService; 035 import org.kuali.rice.kew.service.KEWServiceLocator; 036 import org.kuali.rice.kew.api.KewApiConstants; 037 038 import java.util.ArrayList; 039 import java.util.HashSet; 040 import java.util.Iterator; 041 import java.util.LinkedList; 042 import java.util.List; 043 import java.util.Set; 044 045 046 /** 047 * A WorkflowEngine implementation which orchestrates the document through the blanket approval process. 048 * 049 * @author Kuali Rice Team (rice.collab@kuali.org) 050 */ 051 public class BlanketApproveEngine extends StandardWorkflowEngine { 052 053 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BlanketApproveEngine.class); 054 055 056 BlanketApproveEngine(RouteNodeService routeNodeService, RouteHeaderService routeHeaderService, 057 ParameterService parameterService, OrchestrationConfig config) { 058 super(routeNodeService, routeHeaderService, parameterService, config); 059 } 060 061 /** 062 * Orchestrates the document through the blanket approval process. The termination of the process is keyed off of the Set of node names. If there are no node names, then the document will be blanket approved past the terminal node(s) in the document. 063 */ 064 public void process(String documentId, String nodeInstanceId) throws Exception { 065 if (documentId == null) { 066 throw new IllegalArgumentException("Cannot process a null document id."); 067 } 068 MDC.put("docId", documentId); 069 RouteContext context = RouteContext.getCurrentRouteContext(); 070 try { 071 KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId, true); 072 if ( LOG.isInfoEnabled() ) { 073 LOG.info("Processing document for Blanket Approval: " + documentId + " : " + nodeInstanceId); 074 } 075 DocumentRouteHeaderValue document = getRouteHeaderService().getRouteHeader(documentId); 076 if (!document.isRoutable()) { 077 LOG.debug("Document not routable so returning with doing no action"); 078 return; 079 } 080 List<RouteNodeInstance> activeNodeInstances = new ArrayList<RouteNodeInstance>(); 081 if (nodeInstanceId == null) { 082 activeNodeInstances.addAll(getRouteNodeService().getActiveNodeInstances(documentId)); 083 } else { 084 RouteNodeInstance instanceNode = getRouteNodeService().findRouteNodeInstanceById(nodeInstanceId); 085 if (instanceNode == null) { 086 throw new IllegalArgumentException("Invalid node instance id: " + nodeInstanceId); 087 } 088 activeNodeInstances.add(instanceNode); 089 } 090 List<RouteNodeInstance> nodeInstancesToProcess = determineNodeInstancesToProcess(activeNodeInstances, config.getDestinationNodeNames()); 091 092 093 context.setDoNotSendApproveNotificationEmails(true); 094 context.setDocument(document); 095 context.setEngineState(new EngineState()); 096 NotificationContext notifyContext = null; 097 if (config.isSendNotifications()) { 098 notifyContext = new NotificationContext(KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, config.getCause().getPrincipal(), config.getCause().getActionTaken()); 099 } 100 lockAdditionalDocuments(document); 101 try { 102 List<ProcessEntry> processingQueue = new LinkedList<ProcessEntry>(); 103 for (RouteNodeInstance nodeInstancesToProcesses : nodeInstancesToProcess) 104 { 105 processingQueue.add(new ProcessEntry((RouteNodeInstance) nodeInstancesToProcesses)); 106 } 107 Set<String> nodesCompleted = new HashSet<String>(); 108 // check the processingQueue for cases where there are no dest. nodes otherwise check if we've reached 109 // the dest. nodes 110 while (!processingQueue.isEmpty() && !isReachedDestinationNodes(config.getDestinationNodeNames(), nodesCompleted)) { 111 ProcessEntry entry = processingQueue.remove(0); 112 // TODO document magical join node workage (ask Eric) 113 // TODO this has been set arbitrarily high because the implemented processing model here will probably not work for 114 // large parallel object graphs. This needs to be re-evaluated, see KULWF-459. 115 if (entry.getTimesProcessed() > 20) { 116 throw new WorkflowException("Could not process document through to blanket approval." + " Document failed to progress past node " + entry.getNodeInstance().getRouteNode().getRouteNodeName()); 117 } 118 RouteNodeInstance nodeInstance = entry.getNodeInstance(); 119 context.setNodeInstance(nodeInstance); 120 if (config.getDestinationNodeNames().contains(nodeInstance.getName())) { 121 nodesCompleted.add(nodeInstance.getName()); 122 continue; 123 } 124 ProcessContext resultProcessContext = processNodeInstance(context, helper); 125 invokeBlanketApproval(config.getCause(), nodeInstance, notifyContext); 126 if (!resultProcessContext.getNextNodeInstances().isEmpty() || resultProcessContext.isComplete()) { 127 for (Iterator nodeIt = resultProcessContext.getNextNodeInstances().iterator(); nodeIt.hasNext();) { 128 addToProcessingQueue(processingQueue, (RouteNodeInstance) nodeIt.next()); 129 } 130 } else { 131 entry.increment(); 132 processingQueue.add(processingQueue.size(), entry); 133 } 134 } 135 //clear the context so the standard engine can begin routing normally 136 RouteContext.clearCurrentRouteContext(); 137 // continue with normal routing after blanket approve brings us to the correct place 138 // if there is an active approve request this is no-op. 139 super.process(documentId, null); 140 } catch (Exception e) { 141 if (e instanceof RuntimeException) { 142 throw (RuntimeException)e; 143 } else { 144 throw new WorkflowRuntimeException(e.toString(), e); 145 } 146 } 147 } finally { 148 RouteContext.clearCurrentRouteContext(); 149 MDC.remove("docId"); 150 } 151 } 152 153 /** 154 * @return true if all destination node are active but not yet complete - ready for the standard engine to take over the activation process for requests 155 */ 156 private boolean isReachedDestinationNodes(Set destinationNodesNames, Set<String> nodeNamesCompleted) { 157 return !destinationNodesNames.isEmpty() && nodeNamesCompleted.equals(destinationNodesNames); 158 } 159 160 private void addToProcessingQueue(List<ProcessEntry> processingQueue, RouteNodeInstance nodeInstance) { 161 // first, detect if it's already there 162 for (ProcessEntry entry : processingQueue) 163 { 164 if (entry.getNodeInstance().getRouteNodeInstanceId().equals(nodeInstance.getRouteNodeInstanceId())) 165 { 166 entry.setNodeInstance(nodeInstance); 167 return; 168 } 169 } 170 processingQueue.add(processingQueue.size(), new ProcessEntry(nodeInstance)); 171 } 172 173 /** 174 * If there are multiple paths, we need to figure out which ones we need to follow for blanket approval. This method will throw an exception if a node with the given name could not be located in the routing path. This method is written in such a way that it should be impossible for there to be an infinite loop, even if there is extensive looping in the node graph. 175 */ 176 private List<RouteNodeInstance> determineNodeInstancesToProcess(List<RouteNodeInstance> activeNodeInstances, Set nodeNames) throws Exception { 177 if (nodeNames.isEmpty()) { 178 return activeNodeInstances; 179 } 180 List<RouteNodeInstance> nodeInstancesToProcess = new ArrayList<RouteNodeInstance>(); 181 for (Iterator<RouteNodeInstance> iterator = activeNodeInstances.iterator(); iterator.hasNext();) { 182 RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator.next(); 183 if (isNodeNameInPath(nodeNames, nodeInstance)) { 184 nodeInstancesToProcess.add(nodeInstance); 185 } 186 } 187 if (nodeInstancesToProcess.size() == 0) { 188 throw new InvalidActionTakenException("Could not locate nodes with the given names in the blanket approval path '" + printNodeNames(nodeNames) + "'. " + "The document is probably already passed the specified nodes or does not contain the nodes."); 189 } 190 return nodeInstancesToProcess; 191 } 192 193 private boolean isNodeNameInPath(Set nodeNames, RouteNodeInstance nodeInstance) throws Exception { 194 boolean isInPath = false; 195 for (Object nodeName1 : nodeNames) 196 { 197 String nodeName = (String) nodeName1; 198 for (RouteNode nextNode : nodeInstance.getRouteNode().getNextNodes()) 199 { 200 isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, new HashSet<String>()); 201 } 202 } 203 return isInPath; 204 } 205 206 private boolean isNodeNameInPath(String nodeName, RouteNode node, Set<String> inspected) throws Exception { 207 boolean isInPath = !inspected.contains(node.getRouteNodeId()) && node.getRouteNodeName().equals(nodeName); 208 inspected.add(node.getRouteNodeId()); 209 if (helper.isSubProcessNode(node)) { 210 ProcessDefinitionBo subProcess = node.getDocumentType().getNamedProcess(node.getRouteNodeName()); 211 RouteNode subNode = subProcess.getInitialRouteNode(); 212 isInPath = isInPath || isNodeNameInPath(nodeName, subNode, inspected); 213 } 214 for (RouteNode nextNode : node.getNextNodes()) 215 { 216 isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, inspected); 217 } 218 return isInPath; 219 } 220 221 private String printNodeNames(Set nodesNames) { 222 StringBuffer buffer = new StringBuffer(); 223 for (Iterator iterator = nodesNames.iterator(); iterator.hasNext();) { 224 String nodeName = (String) iterator.next(); 225 buffer.append(nodeName); 226 buffer.append((iterator.hasNext() ? ", " : "")); 227 } 228 return buffer.toString(); 229 } 230 231 /** 232 * Invokes the blanket approval for the given node instance. This deactivates all pending approve or complete requests at the node and sends out notifications to the individuals who's requests were trumped by the blanket approve. 233 */ 234 private void invokeBlanketApproval(ActionTakenValue actionTaken, RouteNodeInstance nodeInstance, NotificationContext notifyContext) { 235 List actionRequests = getActionRequestService().findPendingRootRequestsByDocIdAtRouteNode(nodeInstance.getDocumentId(), nodeInstance.getRouteNodeInstanceId()); 236 actionRequests = getActionRequestService().getRootRequests(actionRequests); 237 List<ActionRequestValue> requestsToNotify = new ArrayList<ActionRequestValue>(); 238 for (Iterator iterator = actionRequests.iterator(); iterator.hasNext();) { 239 ActionRequestValue request = (ActionRequestValue) iterator.next(); 240 if (request.isApproveOrCompleteRequest()) { 241 getActionRequestService().deactivateRequest(actionTaken, request); 242 requestsToNotify.add(request); 243 } 244 } 245 if (notifyContext != null) { 246 ActionRequestFactory arFactory = new ActionRequestFactory(RouteContext.getCurrentRouteContext().getDocument(), nodeInstance); 247 KimPrincipalRecipient delegatorRecipient = null; 248 if (actionTaken.getDelegatorPrincipal() != null) { 249 delegatorRecipient = new KimPrincipalRecipient(actionTaken.getDelegatorPrincipal()); 250 } 251 List<ActionRequestValue> notificationRequests = arFactory.generateNotifications(requestsToNotify, notifyContext.getPrincipalTakingAction(), delegatorRecipient, notifyContext.getNotificationRequestCode(), notifyContext.getActionTakenCode()); 252 getActionRequestService().activateRequests(notificationRequests); 253 } 254 } 255 256 private ActionRequestService getActionRequestService() { 257 return KEWServiceLocator.getActionRequestService(); 258 } 259 260 private class ProcessEntry { 261 262 private RouteNodeInstance nodeInstance; 263 private int timesProcessed = 0; 264 265 public ProcessEntry(RouteNodeInstance nodeInstance) { 266 this.nodeInstance = nodeInstance; 267 } 268 269 public RouteNodeInstance getNodeInstance() { 270 return nodeInstance; 271 } 272 273 public void setNodeInstance(RouteNodeInstance nodeInstance) { 274 this.nodeInstance = nodeInstance; 275 } 276 277 public void increment() { 278 timesProcessed++; 279 } 280 281 public int getTimesProcessed() { 282 return timesProcessed; 283 } 284 285 } 286 287 }