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.parameter.ParameterService; 020import org.kuali.rice.kew.actionrequest.ActionRequestFactory; 021import org.kuali.rice.kew.actionrequest.ActionRequestValue; 022import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient; 023import org.kuali.rice.kew.actionrequest.service.ActionRequestService; 024import org.kuali.rice.kew.actions.NotificationContext; 025import org.kuali.rice.kew.actiontaken.ActionTakenValue; 026import org.kuali.rice.kew.api.WorkflowRuntimeException; 027import org.kuali.rice.kew.api.exception.InvalidActionTakenException; 028import org.kuali.rice.kew.api.exception.WorkflowException; 029import org.kuali.rice.kew.engine.node.ProcessDefinitionBo; 030import org.kuali.rice.kew.engine.node.RouteNode; 031import org.kuali.rice.kew.engine.node.RouteNodeInstance; 032import org.kuali.rice.kew.engine.node.service.RouteNodeService; 033import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 034import org.kuali.rice.kew.routeheader.service.RouteHeaderService; 035import org.kuali.rice.kew.service.KEWServiceLocator; 036import org.kuali.rice.kew.api.KewApiConstants; 037 038import java.util.ArrayList; 039import java.util.HashSet; 040import java.util.Iterator; 041import java.util.LinkedList; 042import java.util.List; 043import 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 */ 051public 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 070 try { 071 RouteContext context = RouteContext.getCurrentRouteContext(); 072 KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId); 073 if ( LOG.isInfoEnabled() ) { 074 LOG.info("Processing document for Blanket Approval: " + documentId + " : " + nodeInstanceId); 075 } 076 DocumentRouteHeaderValue document = getRouteHeaderService().getRouteHeader(documentId, true); 077 if (!document.isRoutable()) { 078 //KULRICE-12283: Modified this message so it appears at a WARN level so we get better feedback if this action is skipped 079 LOG.warn("Document not routable so returning with doing no action"); 080 return; 081 } 082 List<RouteNodeInstance> activeNodeInstances = new ArrayList<RouteNodeInstance>(); 083 if (nodeInstanceId == null) { 084 activeNodeInstances.addAll(getRouteNodeService().getActiveNodeInstances(documentId)); 085 } else { 086 RouteNodeInstance instanceNode = getRouteNodeService().findRouteNodeInstanceById(nodeInstanceId); 087 if (instanceNode == null) { 088 throw new IllegalArgumentException("Invalid node instance id: " + nodeInstanceId); 089 } 090 activeNodeInstances.add(instanceNode); 091 } 092 List<RouteNodeInstance> nodeInstancesToProcess = determineNodeInstancesToProcess(activeNodeInstances, config.getDestinationNodeNames()); 093 094 095 context.setDoNotSendApproveNotificationEmails(true); 096 context.setDocument(document); 097 context.setEngineState(new EngineState()); 098 NotificationContext notifyContext = null; 099 if (config.isSendNotifications()) { 100 notifyContext = new NotificationContext(KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, config.getCause().getPrincipal(), config.getCause().getActionTaken()); 101 } 102 lockAdditionalDocuments(document); 103 try { 104 List<ProcessEntry> processingQueue = new LinkedList<ProcessEntry>(); 105 for (RouteNodeInstance nodeInstancesToProcesses : nodeInstancesToProcess) 106 { 107 processingQueue.add(new ProcessEntry((RouteNodeInstance) nodeInstancesToProcesses)); 108 } 109 Set<String> nodesCompleted = new HashSet<String>(); 110 // check the processingQueue for cases where there are no dest. nodes otherwise check if we've reached 111 // the dest. nodes 112 while (!processingQueue.isEmpty() && !isReachedDestinationNodes(config.getDestinationNodeNames(), nodesCompleted)) { 113 ProcessEntry entry = processingQueue.remove(0); 114 // TODO document magical join node workage (ask Eric) 115 // TODO this has been set arbitrarily high because the implemented processing model here will probably not work for 116 // large parallel object graphs. This needs to be re-evaluated, see KULWF-459. 117 if (entry.getTimesProcessed() > 20) { 118 throw new WorkflowException("Could not process document through to blanket approval." + " Document failed to progress past node " + entry.getNodeInstance().getRouteNode().getRouteNodeName()); 119 } 120 RouteNodeInstance nodeInstance = entry.getNodeInstance(); 121 context.setNodeInstance(nodeInstance); 122 if (config.getDestinationNodeNames().contains(nodeInstance.getName())) { 123 nodesCompleted.add(nodeInstance.getName()); 124 continue; 125 } 126 ProcessContext resultProcessContext = processNodeInstance(context, helper); 127 invokeBlanketApproval(config.getCause(), nodeInstance, notifyContext); 128 if (!resultProcessContext.getNextNodeInstances().isEmpty() || resultProcessContext.isComplete()) { 129 for (Iterator nodeIt = resultProcessContext.getNextNodeInstances().iterator(); nodeIt.hasNext();) { 130 addToProcessingQueue(processingQueue, (RouteNodeInstance) nodeIt.next()); 131 } 132 } else { 133 entry.increment(); 134 processingQueue.add(processingQueue.size(), entry); 135 } 136 } 137 //clear the context so the standard engine can begin routing normally 138 RouteContext.clearCurrentRouteContext(); 139 // continue with normal routing after blanket approve brings us to the correct place 140 // if there is an active approve request this is no-op. 141 super.process(documentId, null); 142 } catch (Exception e) { 143 if (e instanceof RuntimeException) { 144 throw (RuntimeException)e; 145 } else { 146 throw new WorkflowRuntimeException(e.toString(), e); 147 } 148 } 149 } finally { 150 RouteContext.clearCurrentRouteContext(); 151 MDC.remove("docId"); 152 } 153 } 154 155 /** 156 * @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 157 */ 158 private boolean isReachedDestinationNodes(Set destinationNodesNames, Set<String> nodeNamesCompleted) { 159 return !destinationNodesNames.isEmpty() && nodeNamesCompleted.equals(destinationNodesNames); 160 } 161 162 private void addToProcessingQueue(List<ProcessEntry> processingQueue, RouteNodeInstance nodeInstance) { 163 // first, detect if it's already there 164 for (ProcessEntry entry : processingQueue) 165 { 166 if (entry.getNodeInstance().getRouteNodeInstanceId().equals(nodeInstance.getRouteNodeInstanceId())) 167 { 168 entry.setNodeInstance(nodeInstance); 169 return; 170 } 171 } 172 processingQueue.add(processingQueue.size(), new ProcessEntry(nodeInstance)); 173 } 174 175 /** 176 * 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. 177 */ 178 private List<RouteNodeInstance> determineNodeInstancesToProcess(List<RouteNodeInstance> activeNodeInstances, Set nodeNames) throws Exception { 179 if (nodeNames.isEmpty()) { 180 return activeNodeInstances; 181 } 182 List<RouteNodeInstance> nodeInstancesToProcess = new ArrayList<RouteNodeInstance>(); 183 for (Iterator<RouteNodeInstance> iterator = activeNodeInstances.iterator(); iterator.hasNext();) { 184 RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator.next(); 185 if (isNodeNameInPath(nodeNames, nodeInstance)) { 186 nodeInstancesToProcess.add(nodeInstance); 187 } 188 } 189 if (nodeInstancesToProcess.size() == 0) { 190 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."); 191 } 192 return nodeInstancesToProcess; 193 } 194 195 private boolean isNodeNameInPath(Set nodeNames, RouteNodeInstance nodeInstance) throws Exception { 196 boolean isInPath = false; 197 for (Object nodeName1 : nodeNames) 198 { 199 String nodeName = (String) nodeName1; 200 for (RouteNode nextNode : nodeInstance.getRouteNode().getNextNodes()) 201 { 202 isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, new HashSet<String>()); 203 } 204 } 205 return isInPath; 206 } 207 208 private boolean isNodeNameInPath(String nodeName, RouteNode node, Set<String> inspected) throws Exception { 209 boolean isInPath = !inspected.contains(node.getRouteNodeId()) && node.getRouteNodeName().equals(nodeName); 210 inspected.add(node.getRouteNodeId()); 211 if (helper.isSubProcessNode(node)) { 212 ProcessDefinitionBo subProcess = node.getDocumentType().getNamedProcess(node.getRouteNodeName()); 213 RouteNode subNode = subProcess.getInitialRouteNode(); 214 if (subNode != null) { 215 isInPath = isInPath || isNodeNameInPath(nodeName, subNode, inspected); 216 } 217 } 218 for (RouteNode nextNode : node.getNextNodes()) 219 { 220 isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, inspected); 221 } 222 return isInPath; 223 } 224 225 private String printNodeNames(Set nodesNames) { 226 StringBuffer buffer = new StringBuffer(); 227 for (Iterator iterator = nodesNames.iterator(); iterator.hasNext();) { 228 String nodeName = (String) iterator.next(); 229 buffer.append(nodeName); 230 buffer.append((iterator.hasNext() ? ", " : "")); 231 } 232 return buffer.toString(); 233 } 234 235 /** 236 * 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. 237 */ 238 private void invokeBlanketApproval(ActionTakenValue actionTaken, RouteNodeInstance nodeInstance, NotificationContext notifyContext) { 239 List actionRequests = getActionRequestService().findPendingRootRequestsByDocIdAtRouteNode(nodeInstance.getDocumentId(), nodeInstance.getRouteNodeInstanceId()); 240 actionRequests = getActionRequestService().getRootRequests(actionRequests); 241 List<ActionRequestValue> requestsToNotify = new ArrayList<ActionRequestValue>(); 242 for (Iterator iterator = actionRequests.iterator(); iterator.hasNext();) { 243 ActionRequestValue request = (ActionRequestValue) iterator.next(); 244 if (request.isApproveOrCompleteRequest()) { 245 requestsToNotify.add(getActionRequestService().deactivateRequest(actionTaken, request)); 246 } 247 //KULRICE-12283: Added logic to deactivate acks or FYIs if a config option is provided. This will mainly be used when a document is moved. 248 if(request.isAcknowledgeRequest() && config.isDeactivateAcknowledgements()) { 249 getActionRequestService().deactivateRequest(actionTaken, request); 250 } 251 if(request.isFYIRequest() && config.isDeactivateFYIs()) { 252 getActionRequestService().deactivateRequest(actionTaken, request); 253 } 254 } 255 if (notifyContext != null && !requestsToNotify.isEmpty()) { 256 ActionRequestFactory arFactory = new ActionRequestFactory(RouteContext.getCurrentRouteContext().getDocument(), nodeInstance); 257 KimPrincipalRecipient delegatorRecipient = null; 258 if (actionTaken.getDelegatorPrincipal() != null) { 259 delegatorRecipient = new KimPrincipalRecipient(actionTaken.getDelegatorPrincipal()); 260 } 261 List<ActionRequestValue> notificationRequests = arFactory.generateNotifications(requestsToNotify, notifyContext.getPrincipalTakingAction(), delegatorRecipient, notifyContext.getNotificationRequestCode(), notifyContext.getActionTakenCode()); 262 getActionRequestService().activateRequests(notificationRequests); 263 } 264 } 265 266 private ActionRequestService getActionRequestService() { 267 return KEWServiceLocator.getActionRequestService(); 268 } 269 270 private class ProcessEntry { 271 272 private RouteNodeInstance nodeInstance; 273 private int timesProcessed = 0; 274 275 public ProcessEntry(RouteNodeInstance nodeInstance) { 276 this.nodeInstance = nodeInstance; 277 } 278 279 public RouteNodeInstance getNodeInstance() { 280 return nodeInstance; 281 } 282 283 public void setNodeInstance(RouteNodeInstance nodeInstance) { 284 this.nodeInstance = nodeInstance; 285 } 286 287 public void increment() { 288 timesProcessed++; 289 } 290 291 public int getTimesProcessed() { 292 return timesProcessed; 293 } 294 295 } 296 297}