View Javadoc

1   /*
2    * Copyright 2007-2008 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.engine.node;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.log4j.MDC;
20  import org.kuali.rice.core.api.criteria.Predicate;
21  import org.kuali.rice.core.api.criteria.QueryByCriteria;
22  import org.kuali.rice.kew.actionitem.ActionItem;
23  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
24  import org.kuali.rice.kew.api.action.ActionRequestStatus;
25  import org.kuali.rice.kew.doctype.bo.DocumentType;
26  import org.kuali.rice.kew.engine.RouteContext;
27  import org.kuali.rice.kew.engine.RouteHelper;
28  import org.kuali.rice.kew.exception.ResourceUnavailableException;
29  import org.kuali.rice.kew.exception.RouteManagerException;
30  import org.kuali.rice.kew.exception.WorkflowException;
31  import org.kuali.rice.kew.role.RoleRouteModule;
32  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
33  import org.kuali.rice.kew.routemodule.RouteModule;
34  import org.kuali.rice.kew.service.KEWServiceLocator;
35  import org.kuali.rice.kew.util.ClassDumper;
36  import org.kuali.rice.kew.util.KEWConstants;
37  import org.kuali.rice.kew.util.PerformanceLogger;
38  import org.kuali.rice.kim.api.KimConstants;
39  import org.kuali.rice.kim.api.responsibility.Responsibility;
40  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
41  import org.kuali.rice.krad.util.KRADConstants;
42  
43  import java.util.ArrayList;
44  import java.util.Collections;
45  import java.util.Comparator;
46  import java.util.List;
47  
48  import static org.kuali.rice.core.api.criteria.PredicateFactory.and;
49  import static org.kuali.rice.core.api.criteria.PredicateFactory.equal;
50  
51  /**
52   * A node implementation which provides integration with KIM Roles for routing.
53   * Essentially extends RequestsNode and provides a custom RouteModule
54   * implementation.
55   * 
56   * @author Kuali Rice Team (rice.collab@kuali.org)
57   * 
58   */
59  public class RoleNode extends RequestsNode {
60  
61  	private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
62  			.getLogger( RoleNode.class );
63  
64  	@Override
65  	protected RouteModule getRouteModule(RouteContext context) throws Exception {
66  		return new RoleRouteModule();
67  	}
68  	
69  	/**
70  	 * @see org.kuali.rice.kew.engine.node.RequestsNode#processCustom(org.kuali.rice.kew.engine.RouteContext, org.kuali.rice.kew.engine.RouteHelper)
71  	 */
72  	@Override
73  	protected boolean processCustom(RouteContext routeContext, RouteHelper routeHelper) throws Exception {
74  		DocumentRouteHeaderValue document = routeContext.getDocument();
75  		RouteNodeInstance nodeInstance = routeContext.getNodeInstance();
76  		RouteNode node = nodeInstance.getRouteNode();
77  		// while no routable actions are activated and there are more
78  		// routeLevels to process
79  		if ( nodeInstance.isInitial() ) {
80  			if ( LOG.isDebugEnabled() ) {
81  				LOG.debug( "RouteHeader info inside routing loop\n"
82  						+ ClassDumper.dumpFields( routeContext.getDocument() ) );
83  				LOG.debug( "Looking for new actionRequests - routeLevel: "
84  						+ node.getRouteNodeName() );
85  			}
86  			boolean suppressPolicyErrors = isSupressingPolicyErrors( routeContext );
87  			List<ActionRequestValue> requests = getNewActionRequests( routeContext );
88  // Debugging code to force an empty action request				
89  //				if ( document.getDocumentType().getName().equals( "SACC" ) ) {
90  //					LOG.fatal( "DEBUGGING CODE IN PLACE - SACC DOCUMENT ACTION REQUESTS CLEARED" );
91  //					requests.clear();
92  //				}
93  			// for mandatory routes, requests must be generated
94  			if ( requests.isEmpty() && !suppressPolicyErrors) {
95  				Responsibility resp = getFirstResponsibilityWithMandatoryRouteFlag( document, node );
96  				if ( resp != null ) {
97  					throw new RouteManagerException( "No requests generated for KIM Responsibility-based mandatory route.\n" +
98  							"Document Id:    " + document.getDocumentId() + "\n" +
99  							"DocumentType:   " + document.getDocumentType().getName() + "\n" +
100 							"Route Node:     " + node.getRouteNodeName() + "\n" + 
101 							"Responsibility: " + resp,
102 							routeContext );
103 				}
104 			}
105 			// determine if we have any approve requests for FinalApprover
106 			// checks
107 			if ( !suppressPolicyErrors ) {				
108 				verifyFinalApprovalRequest( document, requests, nodeInstance, routeContext );
109 			}
110 		}
111 		return true; // to indicate custom processing performed
112 	}
113 	
114 	/**
115 	 * Checks for any mandatory route responsibilities for the given document type and node.
116 	 * 
117 	 * Stops once it finds a responsibility for the document and node.
118 	 */	
119 	protected Responsibility getFirstResponsibilityWithMandatoryRouteFlag( DocumentRouteHeaderValue document, RouteNode node ) {
120 		// iterate over the document hierarchy
121 		// gather responsibilities - merge based on route level
122 		DocumentType docType = document.getDocumentType();
123 		while ( docType != null ) {
124 			QueryByCriteria.Builder builder = QueryByCriteria.Builder.create();
125             Predicate p = and(
126                 equal("template.namespaceCode", KRADConstants.KUALI_RICE_WORKFLOW_NAMESPACE),
127                 equal("template.name", KEWConstants.DEFAULT_RESPONSIBILITY_TEMPLATE_NAME),
128                 equal("active", "Y"),
129                 equal("attributes[documentTypeName]", docType.getName()),
130                 equal("attributes[routeNodeName]", node.getRouteNodeName())
131             );
132             builder.setPredicates(p);
133 
134             List<Responsibility> responsibilities = KimApiServiceLocator.getResponsibilityService().findResponsibilities(builder.build()).getResults();
135             // once we find a responsibility, stop, since this overrides any parent
136             // responsibilities for this node
137             if ( !responsibilities.isEmpty() ) {
138                 // if any has required=true - return true
139                 for ( Responsibility resp : responsibilities ) {
140                     if ( Boolean.parseBoolean( resp.getAttributes().get( KimConstants.AttributeConstants.REQUIRED ) ) ) {
141                         return resp;
142                     }
143                 }
144                 return null;
145             }
146 			docType = docType.getParentDocType();
147 		}
148 
149 		return null;
150 	}
151 	
152 	/**
153 	 * Activates the action requests that are pending at this routelevel of the
154 	 * document. The requests are processed by priority and then request ID. It
155 	 * is implicit in the access that the requests are activated according to
156 	 * the route level above all.
157 	 * <p>
158 	 * FYI and acknowledgment requests do not cause the processing to stop. Only
159 	 * action requests for approval or completion cause the processing to stop
160 	 * and then only for route level with a serialized activation policy. Only
161 	 * requests at the current document's current route level are activated.
162 	 * Inactive requests at a lower level cause a routing exception.
163 	 * <p>
164 	 * Exception routing and adhoc routing are processed slightly differently.
165 	 * 
166 	 * @return True if the any approval actions were activated.
167 	 * @throws ResourceUnavailableException
168 	 * @throws WorkflowException
169 	 */
170 	@SuppressWarnings("unchecked")
171 	public boolean activateRequests(RouteContext context, DocumentRouteHeaderValue document,
172 			RouteNodeInstance nodeInstance) throws WorkflowException {
173 		MDC.put( "docId", document.getDocumentId() );
174 		PerformanceLogger performanceLogger = new PerformanceLogger( document.getDocumentId() );
175 		List<ActionItem> generatedActionItems = new ArrayList<ActionItem>();
176 		List<ActionRequestValue> requests = new ArrayList<ActionRequestValue>();
177 		if ( context.isSimulation() ) {
178 			for ( ActionRequestValue ar : context.getDocument().getActionRequests() ) {
179 				// logic check below duplicates behavior of the
180 				// ActionRequestService.findPendingRootRequestsByDocIdAtRouteNode(documentId,
181 				// routeNodeInstanceId) method
182 				if ( ar.getCurrentIndicator()
183 						&& (ActionRequestStatus.INITIALIZED.getCode().equals( ar.getStatus() ) || ActionRequestStatus.ACTIVATED.getCode()
184 								.equals( ar.getStatus() ))
185 						&& ar.getNodeInstance().getRouteNodeInstanceId().equals(
186 								nodeInstance.getRouteNodeInstanceId() )
187 						&& ar.getParentActionRequest() == null ) {
188 					requests.add( ar );
189 				}
190 			}
191 			requests.addAll( context.getEngineState().getGeneratedRequests() );
192 		} else {
193 			requests = KEWServiceLocator.getActionRequestService()
194 					.findPendingRootRequestsByDocIdAtRouteNode( document.getDocumentId(),
195 							nodeInstance.getRouteNodeInstanceId() );
196 		}
197 		if ( LOG.isDebugEnabled() ) {
198 			LOG.debug( "Pending Root Requests " + requests.size() );
199 		}
200 		boolean requestActivated = activateRequestsCustom( context, requests, generatedActionItems,
201 				document, nodeInstance );
202 		// now let's send notifications, since this code needs to be able to
203 		// activate each request individually, we need
204 		// to collection all action items and then notify after all have been
205 		// generated
206         notify(context, generatedActionItems, nodeInstance);
207 
208         performanceLogger.log( "Time to activate requests." );
209 		return requestActivated;
210 	}
211 	
212     protected static class RoleRequestSorter implements Comparator<ActionRequestValue> {
213         public int compare(ActionRequestValue ar1, ActionRequestValue ar2) {
214         	int result = 0;
215         	// compare descriptions (only if both not null)
216         	if ( ar1.getResponsibilityDesc() != null && ar2.getResponsibilityDesc() != null ) {
217         		result = ar1.getResponsibilityDesc().compareTo( ar2.getResponsibilityDesc() );
218         	}
219             if ( result != 0 ) return result;
220         	// compare priority
221             result = ar1.getPriority().compareTo(ar2.getPriority());
222             if ( result != 0 ) return result;
223             // compare action request type
224             result = ActionRequestValue.compareActionCode(ar1.getActionRequested(), ar2.getActionRequested(), true);
225             if ( result != 0 ) return result;
226             // compare action request ID
227             if ( (ar1.getActionRequestId() != null) && (ar2.getActionRequestId() != null) ) {
228                 result = ar1.getActionRequestId().compareTo(ar2.getActionRequestId());
229             } else {
230                 // if even one action request id is null at this point return then the two are equal
231                 result = 0;
232             }
233             return result;
234         }
235     }
236     protected static final Comparator<ActionRequestValue> ROLE_REQUEST_SORTER = new RoleRequestSorter();
237 
238 	
239 	protected boolean activateRequestsCustom(RouteContext context,
240 			List<ActionRequestValue> requests, List<ActionItem> generatedActionItems,
241 			DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance)
242 			throws WorkflowException {
243 		Collections.sort( requests, ROLE_REQUEST_SORTER );
244 		String activationType = nodeInstance.getRouteNode().getActivationType();
245 		boolean isParallel = KEWConstants.ROUTE_LEVEL_PARALLEL.equals( activationType );
246 		boolean requestActivated = false;
247 		String groupToActivate = null;
248 		Integer priorityToActivate = null;
249 		for ( ActionRequestValue request : requests ) {
250 			// if a request has already been activated and we are not parallel routing
251 			// or in the simulator, break out of the loop and exit
252 			if ( requestActivated
253 					&& !isParallel
254 					&& (!context.isSimulation() || !context.getActivationContext()
255 							.isActivateRequests()) ) {
256 				break;
257 			}
258 			if ( request.getParentActionRequest() != null || request.getNodeInstance() == null ) {
259 				// 1. disregard request if it's not a top-level request
260 				// 2. disregard request if it's a "future" request and hasn't
261 				// been attached to a node instance yet
262 				continue;
263 			}
264 			if ( request.isApproveOrCompleteRequest() ) {
265 				boolean thisRequestActivated = false;
266 				// capture the priority and grouping information for this request
267 				// We only need this for Approval requests since FYI and ACK requests are non-blocking
268 				if ( priorityToActivate == null ) {
269 				 	priorityToActivate = request.getPriority();
270 				}
271 				if ( groupToActivate == null ) {
272 					groupToActivate = request.getResponsibilityDesc();
273 				}
274 				// check that the given request is found in the current group to activate
275 				// check priority and grouping from the request (stored in the responsibility description)
276 				if ( StringUtils.equals( groupToActivate, request.getResponsibilityDesc() )
277 						&& (
278 								(priorityToActivate != null && request.getPriority() != null && priorityToActivate.equals(request.getPriority()))
279 							||  (priorityToActivate == null && request.getPriority() == null)
280 							)
281 						) {
282 					// if the request is already active, note that we have an active request
283 					// and move on to the next request
284 					if ( request.isActive() ) {
285 						requestActivated = true;
286 						continue;
287 					}
288 					logProcessingMessage( request );
289 					if ( LOG.isDebugEnabled() ) {
290 						LOG.debug( "Activating request: " + request );
291 					}
292 					// this returns true if any requests were activated as a result of this call
293 					thisRequestActivated = activateRequest( context, request, nodeInstance,
294 							generatedActionItems );
295 					requestActivated |= thisRequestActivated;
296 				}
297 				// if this request was not activated and no request has been activated thus far
298 				// then clear out the grouping and priority filters
299 				// as this represents a case where the person with the earlier priority
300 				// did not need to approve for this route level due to taking
301 				// a prior action
302 				if ( !thisRequestActivated && !requestActivated ) {
303 					priorityToActivate = null;
304 					groupToActivate = null;
305 				}
306 			} else {
307 				logProcessingMessage( request );
308 				if ( LOG.isDebugEnabled() ) {
309 					LOG.debug( "Activating request: " + request );
310 				}
311 				requestActivated = activateRequest( context, request, nodeInstance,
312 						generatedActionItems )
313 						|| requestActivated;
314 			}
315 		}
316 		return requestActivated;
317 	}
318 }