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