View Javadoc
1   /**
2    * Copyright 2005-2015 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  
70          try {
71              RouteContext context = RouteContext.getCurrentRouteContext();
72              KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId);
73              if ( LOG.isInfoEnabled() ) {
74              	LOG.info("Processing document for Blanket Approval: " + documentId + " : " + nodeInstanceId);
75              }
76              DocumentRouteHeaderValue document = getRouteHeaderService().getRouteHeader(documentId, true);
77              if (!document.isRoutable()) {
78                  //KULRICE-12283: Modified this message so it appears at a WARN level so we get better feedback if this action is skipped
79                  LOG.warn("Document not routable so returning with doing no action");
80                  return;
81              }
82              List<RouteNodeInstance> activeNodeInstances = new ArrayList<RouteNodeInstance>();
83              if (nodeInstanceId == null) {
84                  activeNodeInstances.addAll(getRouteNodeService().getActiveNodeInstances(documentId));
85              } else {
86                  RouteNodeInstance instanceNode = getRouteNodeService().findRouteNodeInstanceById(nodeInstanceId);
87                  if (instanceNode == null) {
88                      throw new IllegalArgumentException("Invalid node instance id: " + nodeInstanceId);
89                  }
90                  activeNodeInstances.add(instanceNode);
91              }
92              List<RouteNodeInstance> nodeInstancesToProcess = determineNodeInstancesToProcess(activeNodeInstances, config.getDestinationNodeNames());
93  
94  
95              context.setDoNotSendApproveNotificationEmails(true);
96              context.setDocument(document);
97              context.setEngineState(new EngineState());
98              NotificationContext notifyContext = null;
99              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 }