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 }