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 }