001 /**
002 * Copyright 2005-2014 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.rice.kew.engine.node;
017
018 import java.util.ArrayList;
019 import java.util.HashSet;
020 import java.util.Iterator;
021 import java.util.List;
022 import java.util.Set;
023
024 import org.apache.commons.collections.CollectionUtils;
025 import org.kuali.rice.coreservice.framework.parameter.ParameterService;
026 import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
027 import org.kuali.rice.kew.actionrequest.ActionRequestValue;
028 import org.kuali.rice.kew.engine.RouteContext;
029 import org.kuali.rice.kew.engine.RouteHelper;
030 import org.kuali.rice.kew.exception.RouteManagerException;
031 import org.kuali.rice.kew.api.exception.WorkflowException;
032 import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
033 import org.kuali.rice.kew.routemodule.RouteModule;
034 import org.kuali.rice.kew.service.KEWServiceLocator;
035 import org.kuali.rice.kew.util.ClassDumper;
036 import org.kuali.rice.kew.api.KewApiConstants;
037 import org.kuali.rice.krad.util.KRADConstants;
038
039 /**
040 * A node which generates {@link ActionRequestValue} objects from a
041 * {@link RouteModule}.
042 *
043 * @author Kuali Rice Team (rice.collab@kuali.org)
044 */
045 public class RequestsNode extends RequestActivationNode {
046
047 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
048 .getLogger( RequestsNode.class );
049
050 protected static final String SUPPRESS_POLICY_ERRORS_KEY = "_suppressPolicyErrorsRequestActivationNode";
051
052 public final SimpleResult process(RouteContext routeContext, RouteHelper routeHelper)
053 throws Exception {
054 try {
055 if (processCustom(routeContext, routeHelper)) {
056 return super.process(routeContext, routeHelper);
057 }
058 RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
059 boolean isInitial = nodeInstance.isInitial();
060 int currentIteration = 0;
061 List<ActionRequestValue> requestsGenerated = new ArrayList<ActionRequestValue>();
062 while (true) {
063 detectRunawayProcess(routeContext, currentIteration++);
064 if (isInitial) {
065 requestsGenerated = generateRequests(routeContext);
066 // need to set to false because we could iterate more than once here when the node is still in
067 // "initial" state
068 isInitial = false;
069 }
070 SimpleResult simpleResult = super.process(routeContext, routeHelper);
071 if (simpleResult.isComplete()) {
072 RouteModule routeModule = getRouteModule(routeContext);
073 boolean moreRequestsAvailable = routeModule.isMoreRequestsAvailable(routeContext);
074 if (!moreRequestsAvailable) {
075 applyPoliciesOnExit(requestsGenerated, routeContext);
076 return simpleResult;
077 } else {
078 requestsGenerated = generateRequests(routeContext);
079 }
080 } else {
081 return simpleResult;
082 }
083 }
084 } catch ( RouteManagerException ex ) {
085 // just re-throw - no need to wrap
086 throw ex;
087 } catch ( Exception e ) {
088 LOG.error( "Caught exception routing", e );
089 throw new RouteManagerException( e.getMessage(), e, routeContext );
090 }
091 }
092
093 protected List<ActionRequestValue> generateRequests(RouteContext routeContext) throws Exception {
094 DocumentRouteHeaderValue document = routeContext.getDocument();
095 RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
096 RouteNode node = nodeInstance.getRouteNode();
097 if (LOG.isDebugEnabled()) {
098 LOG.debug("RouteHeader info inside routing loop\n" + ClassDumper.dumpFields(document));
099 LOG.debug("Looking for new actionRequests - routeLevel: " + node.getRouteNodeName());
100 }
101 boolean suppressPolicyErrors = isSupressingPolicyErrors(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() && !isSupressingPolicyErrors(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 isSupressingPolicyErrors(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 setSupressPolicyErrors(RouteContext routeContext) {
300 routeContext.getParameters().put( SUPPRESS_POLICY_ERRORS_KEY, Boolean.TRUE );
301 }
302
303 protected ParameterService getParameterService() {
304 return CoreFrameworkServiceLocator.getParameterService();
305 }
306 }