View Javadoc

1   /**
2    * Copyright 2005-2013 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 			for ( ActionRequestValue actionRequest : requests ) {
187 				if ( LOG.isDebugEnabled() ) {
188 					LOG.debug( "Request generated by RouteModule '" + routeModule + "' for node "
189 							+ nodeInstance + ":" + actionRequest );
190 				}
191 				actionRequest = KEWServiceLocator.getActionRequestService()
192 						.initializeActionRequestGraph( actionRequest, context.getDocument(),
193 								nodeInstance );
194 				saveActionRequest( context, actionRequest );
195 				newRequests.add( actionRequest );
196 			}
197 		} catch ( WorkflowException ex ) {
198 			LOG.warn( "Caught WorkflowException during routing", ex );
199 			throw new RouteManagerException( ex, context );
200 		}
201 		return newRequests;
202 	}
203 
204 	/**
205 	 * Returns the RouteModule which should handle generating requests for this
206 	 * RequestsNode.
207 	 */
208 	protected RouteModule getRouteModule(RouteContext context) throws Exception {
209 		return KEWServiceLocator.getRouteModuleService().findRouteModule(
210 				context.getNodeInstance().getRouteNode() );
211 	}
212 
213 	/**
214 	 * Checks if the document has past the final approver node by walking
215 	 * backward through the previous node instances. Ignores any previous nodes
216 	 * that have been "revoked".
217 	 */
218 	protected boolean isPastFinalApprover(DocumentRouteHeaderValue document,
219 			RouteNodeInstance nodeInstance) {
220 		FinalApproverContext context = new FinalApproverContext();
221 		List revokedNodeInstances = KEWServiceLocator.getRouteNodeService()
222 				.getRevokedNodeInstances( document );
223 		Set revokedNodeInstanceIds = new HashSet();
224 		for ( Iterator iterator = revokedNodeInstances.iterator(); iterator.hasNext(); ) {
225 			RouteNodeInstance revokedNodeInstance = (RouteNodeInstance)iterator.next();
226 			revokedNodeInstanceIds.add( revokedNodeInstance.getRouteNodeInstanceId() );
227 		}
228 		isPastFinalApprover( nodeInstance.getPreviousNodeInstances(), context,
229 				revokedNodeInstanceIds );
230 		return context.isPast;
231 	}
232 
233 	protected void isPastFinalApprover(List previousNodeInstances, FinalApproverContext context,
234 			Set revokedNodeInstanceIds) {
235 		if ( previousNodeInstances != null && !previousNodeInstances.isEmpty() ) {
236 			for ( Iterator iterator = previousNodeInstances.iterator(); iterator.hasNext(); ) {
237 				if ( context.isPast ) {
238 					return;
239 				}
240 				RouteNodeInstance nodeInstance = (RouteNodeInstance)iterator.next();
241 				if ( context.inspected.contains( getKey( nodeInstance ) ) ) {
242 					continue;
243 				} else {
244 					context.inspected.add( getKey( nodeInstance ) );
245 				}
246 				if ( Boolean.TRUE.equals( nodeInstance.getRouteNode().getFinalApprovalInd() ) ) {
247 					// if the node instance has been revoked (by a Return To
248 					// Previous action for example)
249 					// then we don't want to consider that node when we
250 					// determine if we are past final
251 					// approval or not
252 					if ( !revokedNodeInstanceIds.contains( nodeInstance.getRouteNodeInstanceId() ) ) {
253 						context.isPast = true;
254 					}
255 					return;
256 				}
257 				isPastFinalApprover( nodeInstance.getPreviousNodeInstances(), context,
258 						revokedNodeInstanceIds );
259 			}
260 		}
261 	}
262 
263 	/**
264 	 * The method will get a key value which can be used for comparison
265 	 * purposes. If the node instance has a primary key value, it will be
266 	 * returned. However, if the node instance has not been saved to the
267 	 * database (i.e. during a simulation) this method will return the node
268 	 * instance passed in.
269 	 */
270 	protected Object getKey(RouteNodeInstance nodeInstance) {
271 		String id = nodeInstance.getRouteNodeInstanceId();
272 		return (id != null ? (Object)id : (Object)nodeInstance);
273 	}
274 
275     protected void detectRunawayProcess(RouteContext routeContext, int currentIteration) throws NumberFormatException {
276 	    String maxNodesConstant = getParameterService().getParameterValueAsString(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.MAX_NODES_BEFORE_RUNAWAY_PROCESS);
277 	    int maxNodes = (org.apache.commons.lang.StringUtils.isEmpty(maxNodesConstant)) ? 50 : Integer.valueOf(maxNodesConstant);
278 	    if (currentIteration > maxNodes) {
279             throw new RouteManagerException("Detected a runaway process within RequestsNode for document with id '" + routeContext.getDocument().getDocumentId() + "' after " + currentIteration + " iterations.");
280         }
281 	}
282 
283 	protected class FinalApproverContext {
284 		public Set inspected = new HashSet();
285 
286 		public boolean isPast = false;
287 	}
288 
289 	public static boolean isSuppressingPolicyErrors(RouteContext routeContext) {
290 		Boolean suppressPolicyErrors = (Boolean)routeContext.getParameters().get(
291 				SUPPRESS_POLICY_ERRORS_KEY );
292 		if ( suppressPolicyErrors == null || !suppressPolicyErrors ) {
293 			return false;
294 		}
295 		return true;
296 	}
297 
298 	@SuppressWarnings("unchecked")
299 	public static void setSuppressPolicyErrors(RouteContext routeContext) {
300 		routeContext.getParameters().put( SUPPRESS_POLICY_ERRORS_KEY, Boolean.TRUE );
301 	}
302 
303     protected ParameterService getParameterService() {
304 		return CoreFrameworkServiceLocator.getParameterService();
305 	}
306 }