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.node;
17  
18  import org.apache.log4j.MDC;
19  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
20  import org.kuali.rice.kew.api.exception.WorkflowException;
21  import org.kuali.rice.kew.engine.RouteContext;
22  import org.kuali.rice.kew.engine.RouteHelper;
23  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
24  import org.kuali.rice.kew.service.KEWServiceLocator;
25  import org.kuali.rice.kew.api.KewApiConstants;
26  import org.kuali.rice.kew.util.PerformanceLogger;
27  import org.kuali.rice.kew.util.Utilities;
28  
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.Iterator;
32  import java.util.List;
33  
34  
35  /**
36   * A node which will iteratively activate any requests pending on it.  Subclasses are responsible for generating the requests
37   * to be activated, and then delegating to superclass {@link #process(RouteContext, RouteHelper)} to activate
38   * those requests.
39   *
40   * This node can be used to serve multiple batches of requests, and can be re-entered multiple times.  The algorithm it implements is:
41   * 
42   * <ol>
43   *   <li>activate any existing requests (requests may have been generated by an external party)
44   *   <li>while request fulfillment criteria indicates that any pending requests have been satisfied
45   *     <ol>
46   *       <li>generate new requests</li>
47   *       <li><b>if no requests were generated, we are done, return and transition</b></li>
48   *       <li>otherwise if requests were generated
49   *         <ol>
50   *           <li>activate requests (depending on activation policy, serial or parallel, <i>NOT ALL GENERATED REQUESTS MAY BE ACTIVATED AT THIS POINT</i>)</li>
51   *           <li>determine a request fulfillment criteria</li>
52   *           <li>continue in loop</li>
53   *         </ol>
54   *       </li>
55   *   </li>
56   *   <li>otherwise if pending requests have not been satisfied, block until invoked again</li>
57   * </ol>
58   * This node transitions/completes when there are no remaining <i>blocking</i> action requests (i.e., no approval or
59   * completion requests).
60   *
61   * @author Kuali Rice Team (rice.collab@kuali.org)
62   */
63  public class IteratedRequestActivationNode implements SimpleNode {
64  
65      protected final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(getClass());
66      private static long generatedRequestPriority = 0;
67  
68      protected static interface RequestFulfillmentCriteria {
69          public boolean pendingRequestsAreFulfilled(boolean activatedBlockingRequests, RouteContext routeContext);
70      }
71  
72      private static class AllBlockingRequestsCompleteCriteria implements RequestFulfillmentCriteria {
73          public boolean pendingRequestsAreFulfilled(boolean activatedBlockingRequests, RouteContext routeContext) {
74              // determine whether there are any pending blocking requests
75              if (activatedBlockingRequests) {
76                  // if we just activated blocking requests then surely there are pending blocking requests
77                  assert(activatedBlockingRequests == blockingRequestsArePending(routeContext.getDocument(), routeContext.getNodeInstance())) : "Blocking requests were activated but none were subsequently found";
78                  return false;
79              } else {
80                  // otherwise let's see if there are any pre-existing blocking requests associated with this node instance
81                  return !blockingRequestsArePending(routeContext.getDocument(), routeContext.getNodeInstance());
82              }
83          }
84      }
85  
86      private static class SimulatingCriteria implements RequestFulfillmentCriteria {
87          public boolean pendingRequestsAreFulfilled(boolean activatedBlockingRequests, RouteContext routeContext) {
88              // when simulating, never block for requests to be fulfilled
89              return true;
90          }
91      }
92  
93      public SimpleResult process(RouteContext routeContext, RouteHelper routeHelper) throws Exception {
94          // if previous requests were satisfied (if no requests have been generated yet, this should return true
95          RequestFulfillmentCriteria criteria = getRequestFulfillmentCriteria(routeContext);
96          // generate new requests until they are not satisfied or none are generated
97          // if none are generated then we should transition immediately
98          boolean activatedBlockingRequests = activateRequests(routeContext); // activation should always occur as something might have generated unactived requests
99          boolean initialRequestGeneration = routeContext.getNodeInstance().isInitial();
100         while (criteria.pendingRequestsAreFulfilled(activatedBlockingRequests, routeContext)) {
101             boolean newRequestsGenerated = generateNewRequests(initialRequestGeneration, routeContext, routeHelper);
102             initialRequestGeneration = false;
103 
104             if (!newRequestsGenerated) {
105                 // if the pending requests were fulfilled
106                 // ...and we didn't generate any new requests
107                 // ...then there are no further requests to activate (XXX: WRONG)
108                 // ...and we have no more processing to do
109                 return new SimpleResult(true);
110             }
111 
112             // activate any requests that were generated
113             activatedBlockingRequests = activateRequests(routeContext);
114 
115             // set the request fulfillment criteria for the new set of requests
116             // if we are simulating, always set a criteria that indicates fulfillment
117             if (routeContext.isSimulation()) {
118                 criteria = new SimulatingCriteria();
119             } else {
120                 criteria = getRequestFulfillmentCriteria(routeContext);
121             }
122         }
123         // if we got here, then for some reason pending requests have not been fulfilled
124         // so wait/block for request fulfillment
125         return new SimpleResult(false);
126     }
127 
128     /**
129      * Template method that subclasses should override to indicate when activated requests have been fulfilled.
130      * The default implementation is that all pending blocking requests must be fulfilled.
131      * @return a RequestFulfillmentCriteria that indicates when activated requests have been fulfilled.
132      */
133     protected RequestFulfillmentCriteria getRequestFulfillmentCriteria(RouteContext routeContext) {
134         return new AllBlockingRequestsCompleteCriteria();
135     }
136 
137     /**
138      * Template method that subclasses should override to generates new requests
139      * @param initial whether this is the very first request generation, that is, the first generation in the first invocation of the node
140      * @return whether new requests were generated
141      * @throws Exception 
142      */
143     protected boolean generateNewRequests(boolean initial, RouteContext context, RouteHelper routeHelper) throws WorkflowException, Exception {
144         return false;
145     }
146 
147     /**
148      * Activates any pending requests and returns whether there are outstanding blocking requests
149      * @param context the RouteContext
150      * @throws org.kuali.rice.kew.api.exception.WorkflowException if anything goes wrong...
151      * @return whether there are outstanding blocking requests
152      */
153     protected boolean activateRequests(RouteContext routeContext) throws WorkflowException {
154         DocumentRouteHeaderValue document = routeContext.getDocument();
155         RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
156         if (routeContext.isSimulation()) {
157             // this seems to indicate whether, when we are simulating, to activate requests...
158             if (routeContext.getActivationContext().isActivateRequests()) {
159                 activateRequests(routeContext, document, nodeInstance);
160             }
161             // if we are in simulation, don't block, just transition out
162             return false;
163         } else {
164             // activate any unactivated pending requests on this node instance
165             return activateRequests(routeContext, document, nodeInstance);
166         }
167     }
168 
169     /**
170      * @return whether there are any pending requests at the given route node instance which are blocking (i.e., 'approve' or 'complete')
171      */
172     private static boolean blockingRequestsArePending(DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance) {
173         // returns blocking requests that are *activated*
174         List<ActionRequestValue> requests = KEWServiceLocator.getActionRequestService().findPendingRootRequestsByDocIdAtRouteNode(document.getDocumentId(), nodeInstance.getRouteNodeInstanceId());
175         boolean blockingRequestsArePending = false;
176         for (ActionRequestValue request: requests) {
177             if (request.isApproveOrCompleteRequest()) {
178                 blockingRequestsArePending = true;
179                 break;
180             }
181         }
182         return blockingRequestsArePending;
183     }
184 
185     /**
186      * Activates the action requests that are pending at this routelevel of the document. The requests are processed by priority and then request ID.
187      * It is implicit in the access that the requests are activated according to the route level above all.
188      * <p>
189      * FYI and acknowledgement requests do not cause the processing to stop. Only action requests for approval or completion cause the processing to
190      * stop and then only for route level with a serialized activation policy. Only requests at the current document's current route level are activated.
191      * Inactive requests at a lower level cause a routing exception.
192      * <p>
193      * Exception routing and adhoc routing are processed slightly differently.
194      * 
195      * 
196      * @param context the RouteContext
197      * @param document the document we are processing
198      * @param nodeInstance the node instance we are processing
199      * @return True if the any blocking actions requests (approve or complete) were activated.
200      * @throws org.kuali.rice.kew.api.exception.ResourceUnavailableException
201      * @throws org.kuali.rice.kew.api.exception.WorkflowException
202      */
203     private boolean activateRequests(RouteContext context, DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance) throws WorkflowException {
204         MDC.put("docId", document.getDocumentId());
205         PerformanceLogger performanceLogger = new PerformanceLogger(document.getDocumentId());
206         List generatedActionItems = new ArrayList();
207         List<ActionRequestValue> requests = KEWServiceLocator.getActionRequestService().findPendingRootRequestsByDocIdAtRouteNode(document.getDocumentId(), nodeInstance.getRouteNodeInstanceId());
208         if (context.isSimulation()) {
209             requests.addAll(context.getEngineState().getGeneratedRequests());
210         }
211         // this will sort higher priority requests to the front
212         // blocking requests are higher priority, so all blocking requests will be
213         // activated before non-blocking requests
214         Collections.sort(requests, new Utilities.PrioritySorter());
215         LOG.info("Pending Root Requests " + requests.size());
216         String activationType = nodeInstance.getRouteNode().getActivationType();
217         boolean isParallel = KewApiConstants.ROUTE_LEVEL_PARALLEL.equals(activationType);
218         boolean activatedApproveRequest = false;
219         for (Iterator iter = requests.iterator(); iter.hasNext();) {
220             if (activatedApproveRequest && !isParallel) {
221                 LOG.info("Already activated an apprve request and serial, so not activating any more");
222                 break;
223             }
224             ActionRequestValue request = (ActionRequestValue) iter.next();
225             LOG.info("ActionRequestValue: " + request);
226             if (request.getParentActionRequest() != null || request.getNodeInstance() == null) {
227                 // 1. disregard request if it's not a top-level request
228                 // 2. disregard request if it's a "future" request and hasn't been attached to a node instance yet
229                 continue; 
230             }
231             if (request.isActive()) {
232                 activatedApproveRequest = activatedApproveRequest || request.isApproveOrCompleteRequest();
233                 continue;
234             }
235             logProcessingMessage(request);   
236             LOG.info("Activating request. " + request);
237             activatedApproveRequest = activateRequest(context, request, nodeInstance, generatedActionItems) || activatedApproveRequest;
238         }
239         // now let's send notifications, since this code needs to be able to activate each request individually, we need
240         // to collection all action items and then notify after all have been generated
241         if (!context.isSimulation()) {
242             KEWServiceLocator.getNotificationService().notify(generatedActionItems);
243         }
244         performanceLogger.log("Time to activate requests.");
245         return activatedApproveRequest;
246     }
247 
248     private boolean activateRequest(RouteContext context, ActionRequestValue actionRequest, RouteNodeInstance nodeInstance, List generatedActionItems) {
249         if (actionRequest.isRoleRequest()) {
250             List actionRequests = KEWServiceLocator.getActionRequestService().findPendingRootRequestsByDocIdAtRouteNode(actionRequest.getDocumentId(), nodeInstance.getRouteNodeInstanceId());
251             for (Iterator iterator = actionRequests.iterator(); iterator.hasNext();) {
252                 ActionRequestValue siblingRequest = (ActionRequestValue) iterator.next();
253                 if (actionRequest.getRoleName().equals(siblingRequest.getRoleName())) {
254                     KEWServiceLocator.getActionRequestService().activateRequestNoNotification(siblingRequest, context.getActivationContext());
255                     // the generated action items can be found in the activation context
256                     generatedActionItems.addAll(context.getActivationContext().getGeneratedActionItems());
257                 }
258             }
259         }
260         KEWServiceLocator.getActionRequestService().activateRequestNoNotification(actionRequest, context.getActivationContext());
261         // the generated action items can be found in the activation context
262         generatedActionItems.addAll(context.getActivationContext().getGeneratedActionItems());
263         return actionRequest.isApproveOrCompleteRequest() && ! actionRequest.isDone();
264     }
265     
266     protected ActionRequestValue saveActionRequest(RouteContext context, ActionRequestValue actionRequest) {
267         if (!context.isSimulation()) {
268             return KEWServiceLocator.getActionRequestService().saveActionRequest(actionRequest);
269         } else {
270             actionRequest.setActionRequestId(String.valueOf(generatedRequestPriority++));
271             context.getEngineState().getGeneratedRequests().add(actionRequest);
272             return actionRequest;
273         }
274         
275     }
276     
277     private void logProcessingMessage(ActionRequestValue request) {
278         //if (LOG.isDebugEnabled()) {
279                 RouteNodeInstance nodeInstance = request.getNodeInstance();
280             StringBuffer buffer = new StringBuffer();
281             buffer.append("Processing AR: ").append(request.getActionRequestId()).append("\n");
282             buffer.append("AR Node Name: ").append(nodeInstance != null ? nodeInstance.getName() : "null").append("\n");
283             buffer.append("AR RouteLevel: ").append(request.getRouteLevel()).append("\n");
284             buffer.append("AR Request Code: ").append(request.getActionRequested()).append("\n");
285             buffer.append("AR Request priority: ").append(request.getPriority()).append("\n");
286             LOG.info(buffer);
287         //}
288     }
289             
290 }