001 /* 002 * Copyright 2005-2007 The Kuali Foundation 003 * 004 * 005 * Licensed under the Educational Community License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.opensource.org/licenses/ecl2.php 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.kuali.rice.kew.engine.node; 018 019 import java.util.ArrayList; 020 import java.util.HashSet; 021 import java.util.Iterator; 022 import java.util.List; 023 import java.util.Set; 024 025 import org.kuali.rice.kew.actionrequest.ActionRequestValue; 026 import org.kuali.rice.kew.engine.RouteContext; 027 import org.kuali.rice.kew.engine.RouteHelper; 028 import org.kuali.rice.kew.exception.ResourceUnavailableException; 029 import org.kuali.rice.kew.exception.RouteManagerException; 030 import org.kuali.rice.kew.exception.WorkflowException; 031 import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 032 import org.kuali.rice.kew.routemodule.RouteModule; 033 import org.kuali.rice.kew.service.KEWServiceLocator; 034 import org.kuali.rice.kew.util.ClassDumper; 035 036 /** 037 * A node which generates {@link ActionRequestValue} objects from a 038 * {@link RouteModule}. 039 * 040 * @author Kuali Rice Team (rice.collab@kuali.org) 041 */ 042 public class RequestsNode extends RequestActivationNode { 043 044 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger 045 .getLogger( RequestsNode.class ); 046 047 protected static final String SUPPRESS_POLICY_ERRORS_KEY = "_suppressPolicyErrorsRequestActivationNode"; 048 049 public final SimpleResult process(RouteContext routeContext, RouteHelper routeHelper) 050 throws Exception { 051 try { 052 if ( !processCustom( routeContext, routeHelper ) ) { 053 DocumentRouteHeaderValue document = routeContext.getDocument(); 054 RouteNodeInstance nodeInstance = routeContext.getNodeInstance(); 055 RouteNode node = nodeInstance.getRouteNode(); 056 // refreshSearchableAttributes(routeContext); 057 // while no routable actions are activated and there are more 058 // routeLevels to process 059 if ( nodeInstance.isInitial() ) { 060 if ( LOG.isDebugEnabled() ) { 061 LOG.debug( "RouteHeader info inside routing loop\n" 062 + ClassDumper.dumpFields( document ) ); 063 LOG.debug( "Looking for new actionRequests - routeLevel: " 064 + node.getRouteNodeName() ); 065 } 066 boolean suppressPolicyErrors = isSupressingPolicyErrors( routeContext ); 067 List<ActionRequestValue> requests = getNewActionRequests( routeContext ); 068 // for mandatory routes, requests must be generated 069 if ( (requests.isEmpty()) && node.getMandatoryRouteInd().booleanValue() 070 && !suppressPolicyErrors ) { 071 LOG.warn( "no requests generated for mandatory route - " 072 + node.getRouteNodeName() ); 073 throw new RouteManagerException( "No requests generated for mandatory route " 074 + node.getRouteNodeName() + ":" + node.getRouteMethodName(), 075 routeContext ); 076 } 077 // determine if we have any approve requests for FinalApprover 078 // checks 079 if ( !suppressPolicyErrors ) { 080 verifyFinalApprovalRequest( document, requests, nodeInstance, routeContext ); 081 } 082 } 083 } 084 return super.process( routeContext, routeHelper ); 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 /** Used by subclasses to replace the functioning of the process method. 095 * 096 * @return <b>true</b> if custom processing was performed and the base implementation 097 * in {@link #process(RouteContext, RouteHelper)} should be skipped. 098 */ 099 protected boolean processCustom(RouteContext routeContext, RouteHelper routeHelper) throws Exception { 100 return false; 101 } 102 103 /** 104 * Verifies the state of the action requests when a final approval action is involved. 105 * 106 * Throws a RouteManagerException if actions were not generated correctly. 107 */ 108 protected void verifyFinalApprovalRequest( DocumentRouteHeaderValue document, List<ActionRequestValue> requests, RouteNodeInstance nodeInstance, RouteContext routeContext ) throws RouteManagerException { 109 boolean pastFinalApprover = isPastFinalApprover( document, nodeInstance ); 110 boolean hasApproveRequest = false; 111 for ( ActionRequestValue actionRequest : requests ) { 112 if ( actionRequest.isApproveOrCompleteRequest() ) { 113 hasApproveRequest = true; 114 break; 115 } 116 } 117 // if final approver route level and no approve request send to 118 // exception routing 119 if ( nodeInstance.getRouteNode().getFinalApprovalInd().booleanValue() ) { 120 // we must have an approve request generated if final 121 // approver level. 122 if ( !hasApproveRequest ) { 123 throw new RouteManagerException( 124 "No Approve Request generated after final approver", routeContext ); 125 } 126 } else if ( pastFinalApprover ) { 127 // we can't allow generation of approve requests after final 128 // approver. This guys going to exception routing. 129 if ( hasApproveRequest ) { 130 throw new RouteManagerException( 131 "Approve Request generated after final approver", routeContext ); 132 } 133 } 134 } 135 136 137 /** 138 * @param routeLevel 139 * Route level for which the action requests will be generated 140 * @param routeHeader 141 * route header for which the action requests are generated 142 * @param saveFlag 143 * if true the new action requests will be saved, if false they 144 * are not written to the db 145 * @return List of ActionRequests - NOTE they are only written to DB if 146 * saveFlag is set 147 * @throws WorkflowException 148 * @throws ResourceUnavailableException 149 */ 150 public List<ActionRequestValue> getNewActionRequests(RouteContext context) throws Exception { 151 RouteNodeInstance nodeInstance = context.getNodeInstance(); 152 String routeMethodName = nodeInstance.getRouteNode().getRouteMethodName(); 153 if ( LOG.isDebugEnabled() ) { 154 LOG.debug( "Looking for action requests in " + routeMethodName + " : " 155 + nodeInstance.getRouteNode().getRouteNodeName() ); 156 } 157 List<ActionRequestValue> newRequests = new ArrayList<ActionRequestValue>(); 158 try { 159 RouteModule routeModule = getRouteModule( context ); 160 List<ActionRequestValue> requests = routeModule.findActionRequests( context ); 161 for ( ActionRequestValue actionRequest : requests ) { 162 if ( LOG.isDebugEnabled() ) { 163 LOG.debug( "Request generated by RouteModule '" + routeModule + "' for node " 164 + nodeInstance + ":" + actionRequest ); 165 } 166 actionRequest = KEWServiceLocator.getActionRequestService() 167 .initializeActionRequestGraph( actionRequest, context.getDocument(), 168 nodeInstance ); 169 saveActionRequest( context, actionRequest ); 170 newRequests.add( actionRequest ); 171 } 172 } catch ( WorkflowException ex ) { 173 LOG.warn( "Caught WorkflowException during routing", ex ); 174 throw new RouteManagerException( ex, context ); 175 } 176 return newRequests; 177 } 178 179 /** 180 * Returns the RouteModule which should handle generating requests for this 181 * RequestsNode. 182 */ 183 protected RouteModule getRouteModule(RouteContext context) throws Exception { 184 return KEWServiceLocator.getRouteModuleService().findRouteModule( 185 context.getNodeInstance().getRouteNode() ); 186 } 187 188 /** 189 * Checks if the document has past the final approver node by walking 190 * backward through the previous node instances. Ignores any previous nodes 191 * that have been "revoked". 192 */ 193 protected boolean isPastFinalApprover(DocumentRouteHeaderValue document, 194 RouteNodeInstance nodeInstance) { 195 FinalApproverContext context = new FinalApproverContext(); 196 List revokedNodeInstances = KEWServiceLocator.getRouteNodeService() 197 .getRevokedNodeInstances( document ); 198 Set revokedNodeInstanceIds = new HashSet(); 199 for ( Iterator iterator = revokedNodeInstances.iterator(); iterator.hasNext(); ) { 200 RouteNodeInstance revokedNodeInstance = (RouteNodeInstance)iterator.next(); 201 revokedNodeInstanceIds.add( revokedNodeInstance.getRouteNodeInstanceId() ); 202 } 203 isPastFinalApprover( nodeInstance.getPreviousNodeInstances(), context, 204 revokedNodeInstanceIds ); 205 return context.isPast; 206 } 207 208 protected void isPastFinalApprover(List previousNodeInstances, FinalApproverContext context, 209 Set revokedNodeInstanceIds) { 210 if ( previousNodeInstances != null && !previousNodeInstances.isEmpty() ) { 211 for ( Iterator iterator = previousNodeInstances.iterator(); iterator.hasNext(); ) { 212 if ( context.isPast ) { 213 return; 214 } 215 RouteNodeInstance nodeInstance = (RouteNodeInstance)iterator.next(); 216 if ( context.inspected.contains( getKey( nodeInstance ) ) ) { 217 continue; 218 } else { 219 context.inspected.add( getKey( nodeInstance ) ); 220 } 221 if ( Boolean.TRUE.equals( nodeInstance.getRouteNode().getFinalApprovalInd() ) ) { 222 // if the node instance has been revoked (by a Return To 223 // Previous action for example) 224 // then we don't want to consider that node when we 225 // determine if we are past final 226 // approval or not 227 if ( !revokedNodeInstanceIds.contains( nodeInstance.getRouteNodeInstanceId() ) ) { 228 context.isPast = true; 229 } 230 return; 231 } 232 isPastFinalApprover( nodeInstance.getPreviousNodeInstances(), context, 233 revokedNodeInstanceIds ); 234 } 235 } 236 } 237 238 /** 239 * The method will get a key value which can be used for comparison 240 * purposes. If the node instance has a primary key value, it will be 241 * returned. However, if the node instance has not been saved to the 242 * database (i.e. during a simulation) this method will return the node 243 * instance passed in. 244 */ 245 protected Object getKey(RouteNodeInstance nodeInstance) { 246 Long id = nodeInstance.getRouteNodeInstanceId(); 247 return (id != null ? (Object)id : (Object)nodeInstance); 248 } 249 250 protected class FinalApproverContext { 251 public Set inspected = new HashSet(); 252 253 public boolean isPast = false; 254 } 255 256 public static boolean isSupressingPolicyErrors(RouteContext routeContext) { 257 Boolean suppressPolicyErrors = (Boolean)routeContext.getParameters().get( 258 SUPPRESS_POLICY_ERRORS_KEY ); 259 if ( suppressPolicyErrors == null || !suppressPolicyErrors ) { 260 return false; 261 } 262 return true; 263 } 264 265 @SuppressWarnings("unchecked") 266 public static void setSupressPolicyErrors(RouteContext routeContext) { 267 routeContext.getParameters().put( SUPPRESS_POLICY_ERRORS_KEY, Boolean.TRUE ); 268 } 269 }