View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.engine;
17  
18  import org.apache.log4j.MDC;
19  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
20  import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
21  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
22  import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
23  import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
24  import org.kuali.rice.kew.actions.NotificationContext;
25  import org.kuali.rice.kew.actiontaken.ActionTakenValue;
26  import org.kuali.rice.kew.api.WorkflowRuntimeException;
27  import org.kuali.rice.kew.api.exception.InvalidActionTakenException;
28  import org.kuali.rice.kew.api.exception.WorkflowException;
29  import org.kuali.rice.kew.engine.node.ProcessDefinitionBo;
30  import org.kuali.rice.kew.engine.node.RouteNode;
31  import org.kuali.rice.kew.engine.node.RouteNodeInstance;
32  import org.kuali.rice.kew.engine.node.service.RouteNodeService;
33  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
34  import org.kuali.rice.kew.routeheader.service.RouteHeaderService;
35  import org.kuali.rice.kew.service.KEWServiceLocator;
36  import org.kuali.rice.kew.api.KewApiConstants;
37  
38  import java.util.ArrayList;
39  import java.util.HashSet;
40  import java.util.Iterator;
41  import java.util.LinkedList;
42  import java.util.List;
43  import java.util.Set;
44  
45  
46  /**
47   * A WorkflowEngine implementation which orchestrates the document through the blanket approval process.
48   *
49   * @author Kuali Rice Team (rice.collab@kuali.org)
50   */
51  public class BlanketApproveEngine extends StandardWorkflowEngine {
52  	
53  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(BlanketApproveEngine.class);
54  
55  
56      BlanketApproveEngine(RouteNodeService routeNodeService, RouteHeaderService routeHeaderService, 
57              ParameterService parameterService, OrchestrationConfig config) {
58          super(routeNodeService, routeHeaderService, parameterService, config);
59      }
60  
61      /**
62       * 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.
63       */
64      public void process(String documentId, String nodeInstanceId) throws Exception {
65          if (documentId == null) {
66              throw new IllegalArgumentException("Cannot process a null document id.");
67          }
68          MDC.put("docId", documentId);
69          RouteContext context = RouteContext.getCurrentRouteContext();
70          try {
71              KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId);
72              if ( LOG.isInfoEnabled() ) {
73              	LOG.info("Processing document for Blanket Approval: " + documentId + " : " + nodeInstanceId);
74              }
75              DocumentRouteHeaderValue document = getRouteHeaderService().getRouteHeader(documentId, true);
76              if (!document.isRoutable()) {
77                  LOG.debug("Document not routable so returning with doing no action");
78                  return;
79              }
80              List<RouteNodeInstance> activeNodeInstances = new ArrayList<RouteNodeInstance>();
81              if (nodeInstanceId == null) {
82                  activeNodeInstances.addAll(getRouteNodeService().getActiveNodeInstances(documentId));
83              } else {
84                  RouteNodeInstance instanceNode = getRouteNodeService().findRouteNodeInstanceById(nodeInstanceId);
85                  if (instanceNode == null) {
86                      throw new IllegalArgumentException("Invalid node instance id: " + nodeInstanceId);
87                  }
88                  activeNodeInstances.add(instanceNode);
89              }
90              List<RouteNodeInstance> nodeInstancesToProcess = determineNodeInstancesToProcess(activeNodeInstances, config.getDestinationNodeNames());
91  
92  
93              context.setDoNotSendApproveNotificationEmails(true);
94              context.setDocument(document);
95              context.setEngineState(new EngineState());
96              NotificationContext notifyContext = null;
97              if (config.isSendNotifications()) {
98                  notifyContext = new NotificationContext(KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ, config.getCause().getPrincipal(), config.getCause().getActionTaken());
99              }
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             if (subNode != null) {
213                 isInPath = isInPath || isNodeNameInPath(nodeName, subNode, inspected);
214             }
215         }
216         for (RouteNode nextNode : node.getNextNodes())
217         {
218             isInPath = isInPath || isNodeNameInPath(nodeName, nextNode, inspected);
219         }
220         return isInPath;
221     }
222 
223     private String printNodeNames(Set nodesNames) {
224         StringBuffer buffer = new StringBuffer();
225         for (Iterator iterator = nodesNames.iterator(); iterator.hasNext();) {
226             String nodeName = (String) iterator.next();
227             buffer.append(nodeName);
228             buffer.append((iterator.hasNext() ? ", " : ""));
229         }
230         return buffer.toString();
231     }
232 
233     /**
234      * 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.
235      */
236     private void invokeBlanketApproval(ActionTakenValue actionTaken, RouteNodeInstance nodeInstance, NotificationContext notifyContext) {
237         List actionRequests = getActionRequestService().findPendingRootRequestsByDocIdAtRouteNode(nodeInstance.getDocumentId(), nodeInstance.getRouteNodeInstanceId());
238         actionRequests = getActionRequestService().getRootRequests(actionRequests);
239         List<ActionRequestValue> requestsToNotify = new ArrayList<ActionRequestValue>();
240         for (Iterator iterator = actionRequests.iterator(); iterator.hasNext();) {
241             ActionRequestValue request = (ActionRequestValue) iterator.next();
242             if (request.isApproveOrCompleteRequest()) {
243                 requestsToNotify.add(getActionRequestService().deactivateRequest(actionTaken, request));
244             }
245         }
246         if (notifyContext != null && !requestsToNotify.isEmpty()) {
247         	ActionRequestFactory arFactory = new ActionRequestFactory(RouteContext.getCurrentRouteContext().getDocument(), nodeInstance);
248         	KimPrincipalRecipient delegatorRecipient = null;
249         	if (actionTaken.getDelegatorPrincipal() != null) {
250         		delegatorRecipient = new KimPrincipalRecipient(actionTaken.getDelegatorPrincipal());
251         	}
252         	List<ActionRequestValue> notificationRequests = arFactory.generateNotifications(requestsToNotify, notifyContext.getPrincipalTakingAction(), delegatorRecipient, notifyContext.getNotificationRequestCode(), notifyContext.getActionTakenCode());
253         	getActionRequestService().activateRequests(notificationRequests);
254         }
255     }
256 
257     private ActionRequestService getActionRequestService() {
258         return KEWServiceLocator.getActionRequestService();
259     }
260 
261     private class ProcessEntry {
262 
263         private RouteNodeInstance nodeInstance;
264         private int timesProcessed = 0;
265 
266         public ProcessEntry(RouteNodeInstance nodeInstance) {
267             this.nodeInstance = nodeInstance;
268         }
269 
270         public RouteNodeInstance getNodeInstance() {
271             return nodeInstance;
272         }
273 
274         public void setNodeInstance(RouteNodeInstance nodeInstance) {
275             this.nodeInstance = nodeInstance;
276         }
277 
278         public void increment() {
279             timesProcessed++;
280         }
281 
282         public int getTimesProcessed() {
283             return timesProcessed;
284         }
285 
286     }
287 
288 }