001/** 002 * Copyright 2005-2015 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 */ 016package org.kuali.rice.kew.engine.node; 017 018import java.util.ArrayList; 019import java.util.HashSet; 020import java.util.Iterator; 021import java.util.List; 022import java.util.Set; 023 024import org.apache.commons.collections.CollectionUtils; 025import org.kuali.rice.coreservice.framework.parameter.ParameterService; 026import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator; 027import org.kuali.rice.kew.actionrequest.ActionRequestValue; 028import org.kuali.rice.kew.engine.RouteContext; 029import org.kuali.rice.kew.engine.RouteHelper; 030import org.kuali.rice.kew.exception.RouteManagerException; 031import org.kuali.rice.kew.api.exception.WorkflowException; 032import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 033import org.kuali.rice.kew.routemodule.RouteModule; 034import org.kuali.rice.kew.service.KEWServiceLocator; 035import org.kuali.rice.kew.util.ClassDumper; 036import org.kuali.rice.kew.api.KewApiConstants; 037import 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 */ 045public 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 = 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}