View Javadoc

1   /**
2    * Copyright 2005-2013 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         Predicate p = and(
122                 equal("template.namespaceCode", KRADConstants.KUALI_RICE_WORKFLOW_NAMESPACE),
123                 equal("template.name", KewApiConstants.DEFAULT_RESPONSIBILITY_TEMPLATE_NAME),
124                 equal("active", "Y"),
125                 equal("attributes[routeNodeName]", node.getRouteNodeName())
126                 // KULRICE-8538 -- Check the document type while we're looping through the results below.  If it is added
127                 // into the predicate, no rows are ever returned.
128                 // equal("attributes[documentTypeName]", docType.getName())
129         );
130         QueryByCriteria.Builder builder = QueryByCriteria.Builder.create();
131         builder.setPredicates(p);
132         List<Responsibility> responsibilities = KimApiServiceLocator.getResponsibilityService().findResponsibilities(builder.build()).getResults();
133 
134 
135         DocumentType docType = document.getDocumentType();
136         while ( docType != null ) {
137             // once we find a responsibility, stop, since this overrides any parent
138             // responsibilities for this node
139             if ( !responsibilities.isEmpty() ) {
140                 // if any has required=true - return true
141                 for ( Responsibility resp : responsibilities ) {
142                     String documentTypeName = resp.getAttributes().get( KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME);
143                     if (StringUtils.isNotEmpty(documentTypeName) && StringUtils.equals(documentTypeName, docType.getName())){
144                         if ( Boolean.parseBoolean( resp.getAttributes().get( KimConstants.AttributeConstants.REQUIRED ) ) ) {
145                             return resp;
146                         }
147                     }
148                 }
149                 return null;
150             }
151 			docType = docType.getParentDocType();
152 		}
153 
154 		return null;
155 	}
156 
157 	/**
158 	 * Activates the action requests that are pending at this routelevel of the
159 	 * document. The requests are processed by priority and then request ID. It
160 	 * is implicit in the access that the requests are activated according to
161 	 * the route level above all.
162 	 * <p>
163 	 * FYI and acknowledgment requests do not cause the processing to stop. Only
164 	 * action requests for approval or completion cause the processing to stop
165 	 * and then only for route level with a serialized activation policy. Only
166 	 * requests at the current document's current route level are activated.
167 	 * Inactive requests at a lower level cause a routing exception.
168 	 * <p>
169 	 * Exception routing and adhoc routing are processed slightly differently.
170 	 * 
171 	 * @return True if the any approval actions were activated.
172 	 * @throws org.kuali.rice.kew.api.exception.ResourceUnavailableException
173 	 * @throws WorkflowException
174 	 */
175 	@SuppressWarnings("unchecked")
176 	public boolean activateRequests(RouteContext context, DocumentRouteHeaderValue document,
177 			RouteNodeInstance nodeInstance) throws WorkflowException {
178 		MDC.put( "docId", document.getDocumentId() );
179 		PerformanceLogger performanceLogger = new PerformanceLogger( document.getDocumentId() );
180 		List<ActionItem> generatedActionItems = new ArrayList<ActionItem>();
181 		List<ActionRequestValue> requests = new ArrayList<ActionRequestValue>();
182 		if ( context.isSimulation() ) {
183 			for ( ActionRequestValue ar : context.getDocument().getActionRequests() ) {
184 				// logic check below duplicates behavior of the
185 				// ActionRequestService.findPendingRootRequestsByDocIdAtRouteNode(documentId,
186 				// routeNodeInstanceId) method
187 				if ( ar.getCurrentIndicator()
188 						&& (ActionRequestStatus.INITIALIZED.getCode().equals( ar.getStatus() ) || ActionRequestStatus.ACTIVATED.getCode()
189 								.equals( ar.getStatus() ))
190 						&& ar.getNodeInstance().getRouteNodeInstanceId().equals(
191 								nodeInstance.getRouteNodeInstanceId() )
192 						&& ar.getParentActionRequest() == null ) {
193 					requests.add( ar );
194 				}
195 			}
196 			requests.addAll( context.getEngineState().getGeneratedRequests() );
197 		} else {
198 			requests = KEWServiceLocator.getActionRequestService()
199 					.findPendingRootRequestsByDocIdAtRouteNode( document.getDocumentId(),
200 							nodeInstance.getRouteNodeInstanceId() );
201 		}
202 		if ( LOG.isDebugEnabled() ) {
203 			LOG.debug( "Pending Root Requests " + requests.size() );
204 		}
205 		boolean requestActivated = activateRequestsCustom( context, requests, generatedActionItems,
206 				document, nodeInstance );
207 		// now let's send notifications, since this code needs to be able to
208 		// activate each request individually, we need
209 		// to collection all action items and then notify after all have been
210 		// generated
211         notify(context, generatedActionItems, nodeInstance);
212 
213         performanceLogger.log( "Time to activate requests." );
214 		return requestActivated;
215 	}
216 	
217     protected static class RoleRequestSorter implements Comparator<ActionRequestValue> {
218         public int compare(ActionRequestValue ar1, ActionRequestValue ar2) {
219         	int result = 0;
220         	// compare descriptions (only if both not null)
221         	if ( ar1.getResponsibilityDesc() != null && ar2.getResponsibilityDesc() != null ) {
222         		result = ar1.getResponsibilityDesc().compareTo( ar2.getResponsibilityDesc() );
223         	}
224             if ( result != 0 ) return result;
225         	// compare priority
226             result = ar1.getPriority().compareTo(ar2.getPriority());
227             if ( result != 0 ) return result;
228             // compare action request type
229             result = ActionRequestValue.compareActionCode(ar1.getActionRequested(), ar2.getActionRequested(), true);
230             if ( result != 0 ) return result;
231             // compare action request ID
232             if ( (ar1.getActionRequestId() != null) && (ar2.getActionRequestId() != null) ) {
233                 result = ar1.getActionRequestId().compareTo(ar2.getActionRequestId());
234             } else {
235                 // if even one action request id is null at this point return then the two are equal
236                 result = 0;
237             }
238             return result;
239         }
240     }
241     protected static final Comparator<ActionRequestValue> ROLE_REQUEST_SORTER = new RoleRequestSorter();
242 
243 	
244 	protected boolean activateRequestsCustom(RouteContext context,
245 			List<ActionRequestValue> requests, List<ActionItem> generatedActionItems,
246 			DocumentRouteHeaderValue document, RouteNodeInstance nodeInstance)
247 			throws WorkflowException {
248 		Collections.sort( requests, ROLE_REQUEST_SORTER );
249 		String activationType = nodeInstance.getRouteNode().getActivationType();
250 		boolean isParallel = KewApiConstants.ROUTE_LEVEL_PARALLEL.equals( activationType );
251 		boolean requestActivated = false;
252 		String groupToActivate = null;
253 		Integer priorityToActivate = null;
254 		for ( ActionRequestValue request : requests ) {
255 			// if a request has already been activated and we are not parallel routing
256 			// or in the simulator, break out of the loop and exit
257 			if ( requestActivated
258 					&& !isParallel
259 					&& (!context.isSimulation() || !context.getActivationContext()
260 							.isActivateRequests()) ) {
261 				break;
262 			}
263 			if ( request.getParentActionRequest() != null || request.getNodeInstance() == null ) {
264 				// 1. disregard request if it's not a top-level request
265 				// 2. disregard request if it's a "future" request and hasn't
266 				// been attached to a node instance yet
267 				continue;
268 			}
269 			if ( request.isApproveOrCompleteRequest() ) {
270 				boolean thisRequestActivated = false;
271 				// capture the priority and grouping information for this request
272 				// We only need this for Approval requests since FYI and ACK requests are non-blocking
273 				if ( priorityToActivate == null ) {
274 				 	priorityToActivate = request.getPriority();
275 				}
276 				if ( groupToActivate == null ) {
277 					groupToActivate = request.getResponsibilityDesc();
278 				}
279 				// check that the given request is found in the current group to activate
280 				// check priority and grouping from the request (stored in the responsibility description)
281 				if ( StringUtils.equals( groupToActivate, request.getResponsibilityDesc() )
282 						&& (
283 								(priorityToActivate != null && request.getPriority() != null && priorityToActivate.equals(request.getPriority()))
284 							||  (priorityToActivate == null && request.getPriority() == null)
285 							)
286 						) {
287 					// if the request is already active, note that we have an active request
288 					// and move on to the next request
289 					if ( request.isActive() ) {
290 						requestActivated = true;
291 						continue;
292 					}
293 					logProcessingMessage( request );
294 					if ( LOG.isDebugEnabled() ) {
295 						LOG.debug( "Activating request: " + request );
296 					}
297 					// this returns true if any requests were activated as a result of this call
298 					thisRequestActivated = activateRequest( context, request, nodeInstance,
299 							generatedActionItems );
300 					requestActivated |= thisRequestActivated;
301 				}
302 				// if this request was not activated and no request has been activated thus far
303 				// then clear out the grouping and priority filters
304 				// as this represents a case where the person with the earlier priority
305 				// did not need to approve for this route level due to taking
306 				// a prior action
307 				if ( !thisRequestActivated && !requestActivated ) {
308 					priorityToActivate = null;
309 					groupToActivate = null;
310 				}
311 			} else {
312 				logProcessingMessage( request );
313 				if ( LOG.isDebugEnabled() ) {
314 					LOG.debug( "Activating request: " + request );
315 				}
316 				requestActivated = activateRequest( context, request, nodeInstance,
317 						generatedActionItems )
318 						|| requestActivated;
319 			}
320 		}
321 		return requestActivated;
322 	}
323 }