View Javadoc

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