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