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 */ 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.apache.commons.lang.ObjectUtils; 026import org.kuali.rice.coreservice.framework.parameter.ParameterService; 027import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator; 028import org.kuali.rice.kew.actionrequest.ActionRequestValue; 029import org.kuali.rice.kew.engine.RouteContext; 030import org.kuali.rice.kew.engine.RouteHelper; 031import org.kuali.rice.kew.exception.RouteManagerException; 032import org.kuali.rice.kew.api.exception.WorkflowException; 033import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 034import org.kuali.rice.kew.routemodule.RouteModule; 035import org.kuali.rice.kew.service.KEWServiceLocator; 036import org.kuali.rice.kew.util.ClassDumper; 037import org.kuali.rice.kew.api.KewApiConstants; 038import org.kuali.rice.krad.util.KRADConstants; 039 040/** 041 * A node which generates {@link ActionRequestValue} objects from a 042 * {@link RouteModule}. 043 * 044 * @author Kuali Rice Team (rice.collab@kuali.org) 045 */ 046public class RequestsNode extends RequestActivationNode { 047 048 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger 049 .getLogger( RequestsNode.class ); 050 051 protected static final String SUPPRESS_POLICY_ERRORS_KEY = "_suppressPolicyErrorsRequestActivationNode"; 052 053 public final SimpleResult process(RouteContext routeContext, RouteHelper routeHelper) 054 throws Exception { 055 try { 056 if (processCustom(routeContext, routeHelper)) { 057 return super.process(routeContext, routeHelper); 058 } 059 RouteNodeInstance nodeInstance = routeContext.getNodeInstance(); 060 boolean isInitial = nodeInstance.isInitial(); 061 int currentIteration = 0; 062 List<ActionRequestValue> requestsGenerated = new ArrayList<ActionRequestValue>(); 063 while (true) { 064 detectRunawayProcess(routeContext, currentIteration++); 065 if (isInitial) { 066 requestsGenerated = generateRequests(routeContext); 067 // need to set to false because we could iterate more than once here when the node is still in 068 // "initial" state 069 isInitial = false; 070 } 071 SimpleResult simpleResult = super.process(routeContext, routeHelper); 072 if (simpleResult.isComplete()) { 073 RouteModule routeModule = getRouteModule(routeContext); 074 boolean moreRequestsAvailable = routeModule.isMoreRequestsAvailable(routeContext); 075 if (!moreRequestsAvailable) { 076 applyPoliciesOnExit(requestsGenerated, routeContext); 077 return simpleResult; 078 } else { 079 requestsGenerated = generateRequests(routeContext); 080 } 081 } else { 082 return simpleResult; 083 } 084 } 085 } catch ( RouteManagerException ex ) { 086 // just re-throw - no need to wrap 087 throw ex; 088 } catch ( Exception e ) { 089 LOG.error( "Caught exception routing", e ); 090 throw new RouteManagerException( e.getMessage(), e, routeContext ); 091 } 092 } 093 094 protected List<ActionRequestValue> generateRequests(RouteContext routeContext) throws Exception { 095 DocumentRouteHeaderValue document = routeContext.getDocument(); 096 RouteNodeInstance nodeInstance = routeContext.getNodeInstance(); 097 RouteNode node = nodeInstance.getRouteNode(); 098 if (LOG.isDebugEnabled()) { 099 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}