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