View Javadoc
1   /**
2    * Copyright 2005-2016 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 java.util.ArrayList;
19  import java.util.HashSet;
20  import java.util.Iterator;
21  import java.util.List;
22  import java.util.Set;
23  
24  import org.apache.commons.collections.CollectionUtils;
25  import org.apache.commons.lang.ObjectUtils;
26  import org.kuali.rice.coreservice.framework.parameter.ParameterService;
27  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
28  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
29  import org.kuali.rice.kew.engine.RouteContext;
30  import org.kuali.rice.kew.engine.RouteHelper;
31  import org.kuali.rice.kew.exception.RouteManagerException;
32  import org.kuali.rice.kew.api.exception.WorkflowException;
33  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
34  import org.kuali.rice.kew.routemodule.RouteModule;
35  import org.kuali.rice.kew.service.KEWServiceLocator;
36  import org.kuali.rice.kew.util.ClassDumper;
37  import org.kuali.rice.kew.api.KewApiConstants;
38  import org.kuali.rice.krad.util.KRADConstants;
39  
40  /**
41   * A node which generates {@link ActionRequestValue} objects from a
42   * {@link RouteModule}.
43   *
44   * @author Kuali Rice Team (rice.collab@kuali.org)
45   */
46  public class RequestsNode extends RequestActivationNode {
47  
48  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
49  			.getLogger( RequestsNode.class );
50  
51  	protected static final String SUPPRESS_POLICY_ERRORS_KEY = "_suppressPolicyErrorsRequestActivationNode";
52  
53  	public final SimpleResult process(RouteContext routeContext, RouteHelper routeHelper)
54  			throws Exception {
55  		try {
56              if (processCustom(routeContext, routeHelper)) {
57                  return super.process(routeContext, routeHelper);
58              }
59              RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
60              boolean isInitial = nodeInstance.isInitial();
61              int currentIteration = 0;
62              List<ActionRequestValue> requestsGenerated = new ArrayList<ActionRequestValue>();
63              while (true) {
64                  detectRunawayProcess(routeContext, currentIteration++);
65                  if (isInitial) {
66                      requestsGenerated = generateRequests(routeContext);
67                      // need to set to false because we could iterate more than once here when the node is still in
68                      // "initial" state
69                      isInitial = false;
70                  }
71                  SimpleResult simpleResult = super.process(routeContext, routeHelper);
72                  if (simpleResult.isComplete()) {
73                      RouteModule routeModule = getRouteModule(routeContext);
74                      boolean moreRequestsAvailable = routeModule.isMoreRequestsAvailable(routeContext);
75                      if (!moreRequestsAvailable) {
76                          applyPoliciesOnExit(requestsGenerated, routeContext);
77                          return simpleResult;
78                      } else {
79                          requestsGenerated = generateRequests(routeContext);
80                      }
81                  } else {
82                      return simpleResult;
83                  }
84              }
85  		} catch ( RouteManagerException ex ) {
86  			// just re-throw - no need to wrap
87  			throw ex;
88  		} catch ( Exception e ) {
89  			LOG.error( "Caught exception routing", e );
90  			throw new RouteManagerException( e.getMessage(), e, routeContext );
91  		}
92  	}
93  
94      protected List<ActionRequestValue> generateRequests(RouteContext routeContext) throws Exception {
95          DocumentRouteHeaderValue document = routeContext.getDocument();
96  		RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
97  		RouteNode node = nodeInstance.getRouteNode();
98          if (LOG.isDebugEnabled()) {
99              LOG.debug("RouteHeader info inside routing loop\n" + ClassDumper.dumpFields(document));
100             LOG.debug("Looking for new actionRequests - routeLevel: " + node.getRouteNodeName());
101         }
102         boolean suppressPolicyErrors = isSuppressingPolicyErrors(routeContext);
103         List<ActionRequestValue> requests = getNewActionRequests(routeContext);
104         // determine if we have any approve requests for FinalApprover checks
105         if (!suppressPolicyErrors) {
106             verifyFinalApprovalRequest(document, requests, nodeInstance, routeContext);
107         }
108         return requests;
109     }
110 
111     /**
112      * Applies policies that should get checked prior to transitioning out of this node.  The default implementation of
113      * this method checks the "mandatory" policy.
114      *
115      * @param requestsGenerated the requests generated on the current iteration of the route module
116      * @param routeContext the current route context
117      */
118     protected void applyPoliciesOnExit(List<ActionRequestValue> requestsGenerated, RouteContext routeContext) {
119         DocumentRouteHeaderValue document = routeContext.getDocument();
120         RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
121         RouteNode node = nodeInstance.getRouteNode();
122         // for mandatory routes, requests must be generated
123         if (node.isMandatory() && !isSuppressingPolicyErrors(routeContext) && CollectionUtils.isEmpty(requestsGenerated)) {
124             List<ActionRequestValue> actionRequests = KEWServiceLocator.getActionRequestService().findRootRequestsByDocIdAtRouteNode(document.getDocumentId(), nodeInstance.getRouteNodeInstanceId());
125             if (actionRequests.isEmpty()) {
126                 LOG.warn("no requests generated for mandatory route - " + node.getRouteNodeName());
127                 throw new RouteManagerException(
128                     "No requests generated for mandatory route " + node.getRouteNodeName() + ":" + node
129                             .getRouteMethodName(), routeContext);
130             }
131         }
132     }
133 
134     /** Used by subclasses to replace the functioning of the process method.
135 	 *
136 	 * @return <b>true</b> if custom processing was performed and the base implementation
137 	 * in {@link #process(RouteContext, RouteHelper)} should be skipped.
138 	 */
139 	protected boolean processCustom(RouteContext routeContext, RouteHelper routeHelper) throws Exception {
140 		return false;
141 	}
142 
143 	/**
144 	 * Verifies the state of the action requests when a final approval action is involved.
145 	 *
146 	 * Throws a RouteManagerException if actions were not generated correctly.
147 	 */
148 	protected void verifyFinalApprovalRequest( DocumentRouteHeaderValue document, List<ActionRequestValue> requests, RouteNodeInstance nodeInstance, RouteContext routeContext ) throws RouteManagerException {
149 		boolean pastFinalApprover = isPastFinalApprover( document, nodeInstance );
150 		boolean hasApproveRequest = false;
151 		for ( ActionRequestValue actionRequest : requests ) {
152 			if ( actionRequest.isApproveOrCompleteRequest() ) {
153 				hasApproveRequest = true;
154 				break;
155 			}
156 		}
157 		// if final approver route level and no approve request send to
158 		// exception routing
159 		if ( nodeInstance.getRouteNode().getFinalApprovalInd().booleanValue() ) {
160 			// we must have an approve request generated if final
161 			// approver level.
162 			if ( !hasApproveRequest ) {
163 				throw new RouteManagerException(
164 						"No Approve Request generated after final approver", routeContext );
165 			}
166 		} else if ( pastFinalApprover ) {
167 			// we can't allow generation of approve requests after final
168 			// approver. This guys going to exception routing.
169 			if ( hasApproveRequest ) {
170 				throw new RouteManagerException(
171 						"Approve Request generated after final approver", routeContext );
172 			}
173 		}
174 	}
175 
176 	public List<ActionRequestValue> getNewActionRequests(RouteContext context) throws Exception {
177 		RouteNodeInstance nodeInstance = context.getNodeInstance();
178 		String routeMethodName = nodeInstance.getRouteNode().getRouteMethodName();
179 		if ( LOG.isDebugEnabled() ) {
180 			LOG.debug( "Looking for action requests in " + routeMethodName + " : "
181 					+ nodeInstance.getRouteNode().getRouteNodeName() );
182 		}
183 		List<ActionRequestValue> newRequests = new ArrayList<ActionRequestValue>();
184 		try {
185 			RouteModule routeModule = getRouteModule( context );
186 			List<ActionRequestValue> requests = routeModule.findActionRequests( context );
187             // route module should only be returning root requests to us, but in case it doesn't...
188             requests = KEWServiceLocator.getActionRequestService().getRootRequests(requests);
189             List<ActionRequestValue> uniqueRequests = new ArrayList<ActionRequestValue>();
190             for ( ActionRequestValue actionRequest : requests ) {
191                 boolean duplicateFound = false;
192                 for (ActionRequestValue uniqueRequest: uniqueRequests ) {
193                     if (isDuplicateActionRequestDetected(uniqueRequest, actionRequest)) {
194                         duplicateFound = true;
195                         break;
196                     }
197                 }
198                 if (!duplicateFound) {
199                     uniqueRequests.add(actionRequest);
200                     duplicateFound = false;
201                 }
202             }
203             for ( ActionRequestValue actionRequest : uniqueRequests ) {
204 				if ( LOG.isDebugEnabled() ) {
205 					LOG.debug( "Request generated by RouteModule '" + routeModule + "' for node "
206 							+ nodeInstance + ":" + actionRequest );
207 				}
208 				actionRequest = KEWServiceLocator.getActionRequestService()
209 						.initializeActionRequestGraph( actionRequest, context.getDocument(),
210 								nodeInstance );
211 				actionRequest = saveActionRequest( context, actionRequest );
212 				newRequests.add( actionRequest );
213 			}
214 		} catch ( WorkflowException ex ) {
215 			LOG.warn( "Caught WorkflowException during routing", ex );
216 			throw new RouteManagerException( ex, context );
217 		}
218 		return newRequests;
219 	}
220     private boolean isDuplicateActionRequestDetected(ActionRequestValue actionRequest, ActionRequestValue actionRequestToCompare) {
221         if ( (ObjectUtils.equals(actionRequest.getActionRequested(), actionRequestToCompare.getActionRequested())) &&
222                 (ObjectUtils.equals(actionRequest.getPrincipalId(), actionRequestToCompare.getPrincipalId())) &&
223                 (ObjectUtils.equals(actionRequest.getStatus(), actionRequestToCompare.getStatus())) &&
224                 (ObjectUtils.equals(actionRequest.getResponsibilityId(), actionRequestToCompare.getResponsibilityId())) &&
225                 (ObjectUtils.equals(actionRequest.getGroupId(), actionRequestToCompare.getGroupId())) &&
226                 (ObjectUtils.equals(actionRequest.getPriority(), actionRequestToCompare.getPriority())) &&
227                 (ObjectUtils.equals(actionRequest.getRouteLevel(), actionRequestToCompare.getRouteLevel())) &&
228                 (ObjectUtils.equals(actionRequest.getResponsibilityDesc(), actionRequestToCompare.getResponsibilityDesc())) &&
229                 (ObjectUtils.equals(actionRequest.getAnnotation(), actionRequestToCompare.getAnnotation())) &&
230                 (ObjectUtils.equals(actionRequest.getForceAction(), actionRequestToCompare.getForceAction())) &&
231                 (ObjectUtils.equals(actionRequest.getQualifiedRoleName(), actionRequestToCompare.getQualifiedRoleName())) &&
232                 (ObjectUtils.equals(actionRequest.getRoleName(), actionRequestToCompare.getRoleName())) &&
233                 (ObjectUtils.equals(actionRequest.getApprovePolicy(), actionRequestToCompare.getApprovePolicy())) &&
234                 (ObjectUtils.equals(actionRequest.getCurrentIndicator(), actionRequestToCompare.getCurrentIndicator())) &&
235                 (ObjectUtils.equals(actionRequest.getNodeInstance(), actionRequestToCompare.getNodeInstance())) &&
236                 (ObjectUtils.equals(actionRequest.getActionTaken(), actionRequestToCompare.getActionTaken())) &&
237                 (ObjectUtils.equals(actionRequest.getDelegationType(), actionRequestToCompare.getDelegationType())) &&
238                 (ObjectUtils.equals(actionRequest.getRuleBaseValuesId(), actionRequestToCompare.getRuleBaseValuesId())) &&
239                 (ObjectUtils.equals(actionRequest.getDisplayStatus(), actionRequestToCompare.getDisplayStatus())) &&
240                 (ObjectUtils.equals(actionRequest.getQualifiedRoleNameLabel(), actionRequestToCompare.getQualifiedRoleNameLabel())) &&
241                 (ObjectUtils.equals(actionRequest.getParentActionRequestId(), actionRequestToCompare.getParentActionRequestId())) &&
242                 (ObjectUtils.equals(actionRequest.getDocVersion(), actionRequestToCompare.getDocVersion())) &&
243                 (ObjectUtils.equals(actionRequest.getActionTakenId(), actionRequestToCompare.getActionTakenId())) &&
244                 (ObjectUtils.equals(actionRequest.getDocumentId(), actionRequestToCompare.getDocumentId())) ) {
245             return true;
246         } else {
247             return false;
248         }
249     }
250 
251     /**
252 	 * Returns the RouteModule which should handle generating requests for this
253 	 * RequestsNode.
254 	 */
255 	protected RouteModule getRouteModule(RouteContext context) throws Exception {
256 		return KEWServiceLocator.getRouteModuleService().findRouteModule(
257 				context.getNodeInstance().getRouteNode() );
258 	}
259 
260 	/**
261 	 * Checks if the document has past the final approver node by walking
262 	 * backward through the previous node instances. Ignores any previous nodes
263 	 * that have been "revoked".
264 	 */
265 	protected boolean isPastFinalApprover(DocumentRouteHeaderValue document,
266 			RouteNodeInstance nodeInstance) {
267 		FinalApproverContext context = new FinalApproverContext();
268 		List revokedNodeInstances = KEWServiceLocator.getRouteNodeService()
269 				.getRevokedNodeInstances( document );
270 		Set revokedNodeInstanceIds = new HashSet();
271 		for ( Iterator iterator = revokedNodeInstances.iterator(); iterator.hasNext(); ) {
272 			RouteNodeInstance revokedNodeInstance = (RouteNodeInstance)iterator.next();
273 			revokedNodeInstanceIds.add( revokedNodeInstance.getRouteNodeInstanceId() );
274 		}
275 		isPastFinalApprover( nodeInstance.getPreviousNodeInstances(), context,
276 				revokedNodeInstanceIds );
277 		return context.isPast;
278 	}
279 
280 	protected void isPastFinalApprover(List previousNodeInstances, FinalApproverContext context,
281 			Set revokedNodeInstanceIds) {
282 		if ( previousNodeInstances != null && !previousNodeInstances.isEmpty() ) {
283 			for ( Iterator iterator = previousNodeInstances.iterator(); iterator.hasNext(); ) {
284 				if ( context.isPast ) {
285 					return;
286 				}
287 				RouteNodeInstance nodeInstance = (RouteNodeInstance)iterator.next();
288 				if ( context.inspected.contains( getKey( nodeInstance ) ) ) {
289 					continue;
290 				} else {
291 					context.inspected.add( getKey( nodeInstance ) );
292 				}
293 				if ( Boolean.TRUE.equals( nodeInstance.getRouteNode().getFinalApprovalInd() ) ) {
294 					// if the node instance has been revoked (by a Return To
295 					// Previous action for example)
296 					// then we don't want to consider that node when we
297 					// determine if we are past final
298 					// approval or not
299 					if ( !revokedNodeInstanceIds.contains( nodeInstance.getRouteNodeInstanceId() ) ) {
300 						context.isPast = true;
301 					}
302 					return;
303 				}
304 				isPastFinalApprover( nodeInstance.getPreviousNodeInstances(), context,
305 						revokedNodeInstanceIds );
306 			}
307 		}
308 	}
309 
310 	/**
311 	 * The method will get a key value which can be used for comparison
312 	 * purposes. If the node instance has a primary key value, it will be
313 	 * returned. However, if the node instance has not been saved to the
314 	 * database (i.e. during a simulation) this method will return the node
315 	 * instance passed in.
316 	 */
317 	protected Object getKey(RouteNodeInstance nodeInstance) {
318 		String id = nodeInstance.getRouteNodeInstanceId();
319 		return (id != null ? (Object)id : (Object)nodeInstance);
320 	}
321 
322     protected void detectRunawayProcess(RouteContext routeContext, int currentIteration) throws NumberFormatException {
323 	    String maxNodesConstant = getParameterService().getParameterValueAsString(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.MAX_NODES_BEFORE_RUNAWAY_PROCESS);
324 	    int maxNodes = (org.apache.commons.lang.StringUtils.isEmpty(maxNodesConstant)) ? 50 : Integer.valueOf(maxNodesConstant);
325 	    if (currentIteration > maxNodes) {
326             throw new RouteManagerException("Detected a runaway process within RequestsNode for document with id '" + routeContext.getDocument().getDocumentId() + "' after " + currentIteration + " iterations.");
327         }
328 	}
329 
330 	protected class FinalApproverContext {
331 		public Set inspected = new HashSet();
332 
333 		public boolean isPast = false;
334 	}
335 
336 	public static boolean isSuppressingPolicyErrors(RouteContext routeContext) {
337 		Boolean suppressPolicyErrors = (Boolean)routeContext.getParameters().get(
338 				SUPPRESS_POLICY_ERRORS_KEY );
339 		if ( suppressPolicyErrors == null || !suppressPolicyErrors ) {
340 			return false;
341 		}
342 		return true;
343 	}
344 
345 	@SuppressWarnings("unchecked")
346 	public static void setSuppressPolicyErrors(RouteContext routeContext) {
347 		routeContext.getParameters().put( SUPPRESS_POLICY_ERRORS_KEY, Boolean.TRUE );
348 	}
349 
350     protected ParameterService getParameterService() {
351 		return CoreFrameworkServiceLocator.getParameterService();
352 	}
353 }