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