View Javadoc

1   /*
2    * Copyright 2005-2007 The Kuali Foundation
3    *
4    *
5    * Licensed under the Educational Community License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.opensource.org/licenses/ecl2.php
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.kuali.rice.kew.rule;
18  
19  import org.apache.commons.lang.ObjectUtils;
20  import org.apache.commons.lang.StringUtils;
21  import org.apache.log4j.Logger;
22  import org.kuali.rice.core.exception.RiceRuntimeException;
23  import org.kuali.rice.core.util.ClassLoaderUtils;
24  import org.kuali.rice.kew.actionrequest.*;
25  import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
26  import org.kuali.rice.kew.engine.RouteContext;
27  import org.kuali.rice.kew.engine.node.NodeState;
28  import org.kuali.rice.kew.engine.node.RouteNode;
29  import org.kuali.rice.kew.engine.node.RouteNodeInstance;
30  import org.kuali.rice.kew.exception.WorkflowException;
31  import org.kuali.rice.kew.exception.WorkflowRuntimeException;
32  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
33  import org.kuali.rice.kew.rule.bo.RuleAttribute;
34  import org.kuali.rice.kew.rule.bo.RuleTemplate;
35  import org.kuali.rice.kew.rule.bo.RuleTemplateAttribute;
36  import org.kuali.rice.kew.rule.service.RuleDelegationService;
37  import org.kuali.rice.kew.rule.service.RuleService;
38  import org.kuali.rice.kew.service.KEWServiceLocator;
39  import org.kuali.rice.kew.user.RoleRecipient;
40  import org.kuali.rice.kew.util.KEWConstants;
41  import org.kuali.rice.kew.util.PerformanceLogger;
42  import org.kuali.rice.kew.util.ResponsibleParty;
43  import org.kuali.rice.kew.util.Utilities;
44  
45  import java.lang.reflect.Method;
46  import java.sql.Timestamp;
47  import java.util.ArrayList;
48  import java.util.Date;
49  import java.util.List;
50  import java.util.Map;
51  
52  
53  /**
54   * Generates Action Requests for a Document using the rule system and the specified
55   * {@link RuleTemplate}.
56   *
57   * @see ActionRequestValue
58   * @see RuleTemplate
59   * @see RuleBaseValues
60   *
61   * @author Kuali Rice Team (rice.collab@kuali.org)
62   */
63  public class FlexRM {
64  
65  	private static final Logger LOG = Logger.getLogger(FlexRM.class);
66  
67  	/**
68  	 * The default type of rule selector implementation to use if none is explicitly
69  	 * specified for the node.
70  	 */
71  	public static final String DEFAULT_RULE_SELECTOR = "Template";
72  	/**
73  	 * Package in which rule selector implementations live
74  	 */
75  	private static final String RULE_SELECTOR_PACKAGE = "org.kuali.rice.kew.rule";
76  	/**
77  	 * The class name suffix all rule selectors should have; e.g. FooRuleSelector
78  	 */
79  	private static final String RULE_SELECTOR_SUFFIX= "RuleSelector";
80  
81  	private final Timestamp effectiveDate;
82  	/**
83  	 * An accumulator that keeps track of the number of rules that have been selected over the lifespan of
84  	 * this FlexRM instance.
85  	 */
86  	private int selectedRules;
87  
88  	public FlexRM() {
89  		this.effectiveDate = null;
90  	}
91  
92  	public FlexRM(Timestamp effectiveDate) {
93  		this.effectiveDate = effectiveDate;
94  	}
95  
96  	/*public List<ActionRequestValue> getActionRequests(DocumentRouteHeaderValue routeHeader, String ruleTemplateName) throws KEWUserNotFoundException, WorkflowException {
97  	return getActionRequests(routeHeader, null, ruleTemplateName);
98      }*/
99  
100 	// loads a RuleSelector implementation
101 	protected RuleSelector loadRuleSelector(RouteNode routeNodeDef, RouteNodeInstance nodeInstance) throws WorkflowException {
102 		// first see if there ruleselector is configured on a nodeinstance basis
103 		NodeState ns = null;
104 		if (nodeInstance != null) {
105 			ns = nodeInstance.getNodeState(KEWConstants.RULE_SELECTOR_NODE_STATE_KEY);
106 		}
107 		String ruleSelectorName = null;
108 		if (ns != null) {
109 			ruleSelectorName = ns.getValue();
110 		} else {
111 			// otherwise pull it from the RouteNode definition/prototype
112 			Map<String, String> nodeCfgParams = Utilities.getKeyValueCollectionAsMap(
113 					routeNodeDef.
114 					getConfigParams());
115 			ruleSelectorName = nodeCfgParams.get(RouteNode.RULE_SELECTOR_CFG_KEY);
116 		}
117 
118 		if (ruleSelectorName == null) {
119 			ruleSelectorName = DEFAULT_RULE_SELECTOR;
120 		}
121 		ruleSelectorName = StringUtils.capitalize(ruleSelectorName);
122 
123 		// load up the rule selection implementation
124 		String className = RULE_SELECTOR_PACKAGE + "." + ruleSelectorName + RULE_SELECTOR_SUFFIX;
125 		Class<?> ruleSelectorClass;
126 		try {
127 			ruleSelectorClass = ClassLoaderUtils.getDefaultClassLoader().loadClass(className);
128 		} catch (ClassNotFoundException cnfe) {
129 			throw new WorkflowException("Rule selector implementation '" + className + "' not found", cnfe);
130 		}
131 		if (!RuleSelector.class.isAssignableFrom(ruleSelectorClass)) {
132 			throw new WorkflowException("Specified class '" + ruleSelectorClass + "' does not implement RuleSelector interface");
133 		}
134 		RuleSelector ruleSelector;
135 		try {
136 			ruleSelector = ((Class<RuleSelector>) ruleSelectorClass).newInstance();
137 		} catch (Exception e) {
138 			if (e instanceof RuntimeException) {
139 				throw (RuntimeException)e;
140 			}
141 			throw new WorkflowException("Error instantiating rule selector implementation '" + ruleSelectorClass + "'", e);
142 		}
143 
144 		return ruleSelector;
145 	}
146 
147 	/**
148 	 * Generates action requests
149 	 * @param routeHeader the document route header
150 	 * @param nodeInstance the route node instance; this may NOT be null
151 	 * @param ruleTemplateName the rule template
152 	 * @return list of action requests
153 	 * @throws WorkflowException
154 	 */
155 	public List<ActionRequestValue> getActionRequests(DocumentRouteHeaderValue routeHeader, RouteNodeInstance nodeInstance, String ruleTemplateName) 
156 			throws WorkflowException {
157 		return getActionRequests(routeHeader, nodeInstance.getRouteNode(), nodeInstance, ruleTemplateName);
158 	}
159 
160 	/**
161 	 * Generates action requests
162 	 * @param routeHeader the document route header
163 	 * @param routeNodeDef the RouteNode definition of the route node instance
164 	 * @param nodeInstance the route node instance; this may be null!
165 	 * @param ruleTemplateName the rule template
166 	 * @return list of action requests
167 	 * @throws WorkflowException
168 	 */
169 	public List<ActionRequestValue> getActionRequests(DocumentRouteHeaderValue routeHeader, RouteNode routeNodeDef, RouteNodeInstance nodeInstance, String ruleTemplateName) 
170 			throws WorkflowException {
171 		RouteContext context = RouteContext.getCurrentRouteContext();
172 		// TODO really the route context just needs to be able to support nested create and clears
173 		// (i.e. a Stack model similar to transaction intercepting in Spring) and we wouldn't have to do this
174 		if (context.getDocument() == null) {
175 			context.setDocument(routeHeader);
176 		}
177 		if (context.getNodeInstance() == null) {
178 			context.setNodeInstance(nodeInstance);
179 		}
180 
181 		LOG.debug("Making action requests for document " + routeHeader.getRouteHeaderId());
182 
183 		RuleSelector ruleSelector = loadRuleSelector(routeNodeDef, nodeInstance);
184 
185 		List<Rule> rules = ruleSelector.selectRules(context, routeHeader, nodeInstance, ruleTemplateName, effectiveDate);
186 
187 		// XXX: FIXME: this is a special case hack to expose info from the default selection implementation
188 		// this is used in exactly one place, RoutingReportAction, to make a distinction between no rules being
189 		// selected, and no rules actually matching when evaluated
190 		// if (numberOfRules == 0) {
191 		//   errors.add(new WorkflowServiceErrorImpl("There are no rules.", "routereport.noRules"));
192 		// } else {
193 		//   errors.add(new WorkflowServiceErrorImpl("There are rules, but no matches.", "routereport.noMatchingRules"));
194 		// }
195 		if (ruleSelector instanceof TemplateRuleSelector) {
196 			selectedRules += ((TemplateRuleSelector) ruleSelector).getNumberOfSelectedRules();
197 		}
198 
199 		PerformanceLogger performanceLogger = new PerformanceLogger();
200 
201 		ActionRequestFactory arFactory = new ActionRequestFactory(routeHeader, context.getNodeInstance());
202 
203 		List<ActionRequestValue> actionRequests = new ArrayList<ActionRequestValue>();
204 		if (rules != null) {
205 			LOG.info("Total number of rules selected by RuleSelector for documentType=" + routeHeader.getDocumentType().getName() + " and ruleTemplate=" + ruleTemplateName + ": " + rules.size());
206 			for (Rule rule: rules) {
207 				RuleExpressionResult result = rule.evaluate(rule, context);
208 				if (result.isSuccess() && result.getResponsibilities() != null) {
209 					// actionRequests.addAll(makeActionRequests(context, rule, routeHeader, null, null));
210 					makeActionRequests(arFactory, result.getResponsibilities(), context, rule.getDefinition(), routeHeader, null, null);
211 				}
212 			}
213 		}
214 		actionRequests = new ArrayList<ActionRequestValue>(arFactory.getRequestGraphs());
215 		performanceLogger.log("Time to make action request for template " + ruleTemplateName);
216 
217 		return actionRequests;
218 	}
219 
220 	public ResponsibleParty resolveResponsibilityId(Long responsibilityId) {
221 		if (responsibilityId == null) {
222 			throw new IllegalArgumentException("A null responsibilityId was passed to resolve responsibility!");
223 		}
224 		RuleResponsibility resp = getRuleService().findRuleResponsibility(responsibilityId);
225 		ResponsibleParty responsibleParty = new ResponsibleParty();
226 		if (resp!=null && resp.isUsingRole()) {
227 			responsibleParty.setRoleName(resp.getResolvedRoleName());
228 		} else if (resp!=null && resp.isUsingWorkflowUser()) {
229 			responsibleParty.setPrincipalId(resp.getRuleResponsibilityName());
230 		} else if (resp!=null && resp.isUsingGroup()) {
231 			responsibleParty.setGroupId(resp.getRuleResponsibilityName());
232 		} else {
233 			throw new RiceRuntimeException("Failed to resolve responsibility from responsibility ID " + responsibilityId + ".  Responsibility was an invalid type: " + resp);
234 		}
235 		return responsibleParty;
236 	}
237 
238 	private void makeActionRequests(ActionRequestFactory arFactory, RouteContext context, RuleBaseValues rule, DocumentRouteHeaderValue routeHeader, ActionRequestValue parentRequest, RuleDelegation ruleDelegation)
239 			throws WorkflowException {
240 
241 		List<RuleResponsibility> responsibilities = rule.getResponsibilities();
242 		makeActionRequests(arFactory, responsibilities, context, rule, routeHeader, parentRequest, ruleDelegation);
243 	}
244 
245 	public void makeActionRequests(ActionRequestFactory arFactory, List<RuleResponsibility> responsibilities, RouteContext context, RuleBaseValues rule, DocumentRouteHeaderValue routeHeader, ActionRequestValue parentRequest, RuleDelegation ruleDelegation)
246 			throws WorkflowException {
247 
248 		//	Set actionRequests = new HashSet();
249         for (RuleResponsibility responsibility : responsibilities)
250         {
251             //	    arFactory = new ActionRequestFactory(routeHeader);
252 
253             if (responsibility.isUsingRole())
254             {
255                 makeRoleActionRequests(arFactory, context, rule, responsibility, routeHeader, parentRequest, ruleDelegation);
256             } else
257             {
258                 makeActionRequest(arFactory, context, rule, routeHeader, responsibility, parentRequest, ruleDelegation);
259             }
260             //	    if (arFactory.getRequestGraph() != null) {
261             //	    actionRequests.add(arFactory.getRequestGraph());
262             //	    }
263         }
264 	}
265 
266 	private void buildDelegationGraph(ActionRequestFactory arFactory, RouteContext context, 
267 			RuleBaseValues delegationRule, DocumentRouteHeaderValue routeHeaderValue, ActionRequestValue parentRequest, RuleDelegation ruleDelegation) throws WorkflowException {
268 		context.setActionRequest(parentRequest);
269 		if (delegationRule.isActive(new Date())) {
270             for (RuleResponsibility delegationResp : delegationRule.getResponsibilities())
271             {
272                 if (delegationResp.isUsingRole())
273                 {
274                     makeRoleActionRequests(arFactory, context, delegationRule, delegationResp, routeHeaderValue, parentRequest, ruleDelegation);
275                 } else if (delegationRule.isMatch(context.getDocumentContent()))
276                 {
277                     makeActionRequest(arFactory, context, delegationRule, routeHeaderValue, delegationResp, parentRequest, ruleDelegation);
278                 }
279             }
280 		}
281 	}
282 
283 	/**
284 	 * Generates action requests for a role responsibility
285 	 */
286 	private void makeRoleActionRequests(ActionRequestFactory arFactory, RouteContext context, 
287 			RuleBaseValues rule, RuleResponsibility resp, DocumentRouteHeaderValue routeHeader, ActionRequestValue parentRequest, 
288 			RuleDelegation ruleDelegation) throws WorkflowException 
289 	{
290 		String roleName = resp.getResolvedRoleName();
291 		RoleAttribute roleAttribute = resp.resolveRoleAttribute();
292 		setRuleAttribute(roleAttribute, rule, resp.getRoleAttributeName());
293 		List<String> qualifiedRoleNames = new ArrayList<String>();
294 		if (parentRequest != null && parentRequest.getQualifiedRoleName() != null) {
295 			qualifiedRoleNames.add(parentRequest.getQualifiedRoleName());
296 		} else {
297 			qualifiedRoleNames.addAll(roleAttribute.getQualifiedRoleNames(roleName, context.getDocumentContent()));
298 		}
299         for (String qualifiedRoleName : qualifiedRoleNames)
300         {
301             if (parentRequest == null && isDuplicateActionRequestDetected(routeHeader, context.getNodeInstance(), resp, qualifiedRoleName))
302             {
303                 continue;
304             }
305 
306             ResolvedQualifiedRole resolvedRole = roleAttribute.resolveQualifiedRole(context, roleName, qualifiedRoleName);
307             RoleRecipient recipient = new RoleRecipient(roleName, qualifiedRoleName, resolvedRole);
308             if (parentRequest == null)
309             {
310                 ActionRequestValue roleRequest = arFactory.addRoleRequest(recipient, resp.getActionRequestedCd(), resp.getApprovePolicy(), resp.getPriority(), resp.getResponsibilityId(), rule
311                         .getForceAction(), rule.getDescription(), rule.getRuleBaseValuesId());
312 // Old, pre 1.0 delegate code, commenting out for now
313 //
314 //				if (resp.isDelegating()) {
315 //					// create delegations for all the children
316 //					for (Iterator iterator = roleRequest.getChildrenRequests().iterator(); iterator.hasNext();) {
317 //						ActionRequestValue request = (ActionRequestValue) iterator.next();
318 //						for (Iterator ruleDelegationIterator = resp.getDelegationRules().iterator(); ruleDelegationIterator.hasNext();) {
319 //							RuleDelegation childRuleDelegation = (RuleDelegation) ruleDelegationIterator.next();
320 //							buildDelegationGraph(arFactory, context, childRuleDelegation.getDelegationRuleBaseValues(), routeHeader, request, childRuleDelegation);
321 //						}
322 //					}
323 //				}
324 
325                 // new Rice 1.0 delegate rule code
326 
327                 List<RuleDelegation> ruleDelegations = getRuleDelegationService().findByResponsibilityId(resp.getResponsibilityId());
328                 if (ruleDelegations != null && !ruleDelegations.isEmpty())
329                 {
330                     // create delegations for all the children
331                     for (ActionRequestValue request : roleRequest.getChildrenRequests())
332                     {
333                         for (RuleDelegation childRuleDelegation : ruleDelegations)
334                         {
335                             buildDelegationGraph(arFactory, context, childRuleDelegation.getDelegationRuleBaseValues(), routeHeader, request, childRuleDelegation);
336                         }
337                     }
338                 }
339 
340             } else
341             {
342                 arFactory.addDelegationRoleRequest(parentRequest, resp.getApprovePolicy(), recipient, resp.getResponsibilityId(), rule.getForceAction(), ruleDelegation.getDelegationType(), rule.getDescription(), rule.getRuleBaseValuesId());
343             }
344         }
345 			}
346 
347 	/**
348 	 * Determines if the attribute has a setRuleAttribute method and then sets the value appropriately if it does.
349 	 */
350 	private void setRuleAttribute(RoleAttribute roleAttribute, RuleBaseValues rule, String roleAttributeName) {
351 		// look for a setRuleAttribute method on the RoleAttribute
352 		Method setRuleAttributeMethod = null;
353 		try {
354 			setRuleAttributeMethod = roleAttribute.getClass().getMethod("setRuleAttribute", RuleAttribute.class);
355 		} catch (NoSuchMethodException e) {}
356 		if (setRuleAttributeMethod == null) {
357 			return;
358 		}
359 		// find the RuleAttribute by looking through the RuleTemplate
360 		RuleTemplate ruleTemplate = rule.getRuleTemplate();
361 		if (ruleTemplate != null) {
362             for (RuleTemplateAttribute ruleTemplateAttribute : ruleTemplate.getActiveRuleTemplateAttributes())
363             {
364                 RuleAttribute ruleAttribute = ruleTemplateAttribute.getRuleAttribute();
365                 if (ruleAttribute.getClassName().equals(roleAttributeName))
366                 {
367                     // this is our RuleAttribute!
368                     try
369                     {
370                         setRuleAttributeMethod.invoke(roleAttribute, ruleAttribute);
371                         break;
372                     } catch (Exception e)
373                     {
374                         throw new WorkflowRuntimeException("Failed to set RuleAttribute on our RoleAttribute!", e);
375                     }
376                 }
377             }
378 		}
379 	}
380 
381 	/**
382 	 * Generates action requests for a non-role responsibility, either a user or workgroup
383      * @throws org.kuali.rice.kew.exception.WorkflowException
384      */
385 	private void makeActionRequest(ActionRequestFactory arFactory, RouteContext context, RuleBaseValues rule, DocumentRouteHeaderValue routeHeader, RuleResponsibility resp, ActionRequestValue parentRequest,
386 			RuleDelegation ruleDelegation) throws WorkflowException {
387 		if (parentRequest == null && isDuplicateActionRequestDetected(routeHeader, context.getNodeInstance(), resp, null)) {
388 			return;
389 		}
390 		Recipient recipient;
391 		if (resp.isUsingWorkflowUser()) {
392 	    recipient = new KimPrincipalRecipient(resp.getPrincipal());
393 	} else if (resp.isUsingGroup()) {
394 	    recipient = new KimGroupRecipient(resp.getGroup());
395 		} else {
396 		throw new RiceRuntimeException("Illegal rule responsibility type encountered: " + resp.getRuleResponsibilityType());
397 		}
398 		ActionRequestValue actionRequest;
399 		if (parentRequest == null) {
400 			actionRequest = arFactory.addRootActionRequest(resp.getActionRequestedCd(),
401 					resp.getPriority(),
402 					recipient,
403 					rule.getDescription(),
404 					resp.getResponsibilityId(),
405 					rule.getForceAction(),
406 					resp.getApprovePolicy(),
407 					rule.getRuleBaseValuesId());
408 
409 			// old, pre 1.0 delegation code, commented out for now
410 			
411 //			if (resp.isDelegating()) {
412 //			for (Iterator iterator = resp.getDelegationRules().iterator(); iterator.hasNext();) {
413 //				RuleDelegation childRuleDelegation = (RuleDelegation) iterator.next();
414 //				buildDelegationGraph(arFactory, context, childRuleDelegation.getDelegationRuleBaseValues(), routeHeader, actionRequest, childRuleDelegation);
415 //			}
416 //		}
417 
418 			
419 			// new Rice 1.0 delegate rule code
420 			
421 			List<RuleDelegation> ruleDelegations = getRuleDelegationService().findByResponsibilityId(resp.getResponsibilityId());
422 			if (ruleDelegations != null && !ruleDelegations.isEmpty()) {
423 				for (RuleDelegation childRuleDelegation : ruleDelegations) {
424 					buildDelegationGraph(arFactory, context, childRuleDelegation.getDelegationRuleBaseValues(), routeHeader, actionRequest, childRuleDelegation);
425 				}
426 			}
427 			
428 		} else {
429 			arFactory.addDelegationRequest(parentRequest, recipient, resp.getResponsibilityId(), rule.getForceAction(), ruleDelegation.getDelegationType(), rule.getDescription(), rule.getRuleBaseValuesId());
430 		}
431 	}
432 
433 	private boolean isDuplicateActionRequestDetected(DocumentRouteHeaderValue routeHeader, RouteNodeInstance nodeInstance, RuleResponsibility resp, String qualifiedRoleName) {
434 		List<ActionRequestValue> requests = getActionRequestService().findByStatusAndDocId(KEWConstants.ACTION_REQUEST_DONE_STATE, routeHeader.getRouteHeaderId());
435         for (ActionRequestValue request : requests)
436         {
437             if (((nodeInstance != null && request.getNodeInstance() != null && request.getNodeInstance().getRouteNodeInstanceId().equals(nodeInstance.getRouteNodeInstanceId())) || request
438                     .getRouteLevel().equals(routeHeader.getDocRouteLevel()))
439                     && request.getResponsibilityId().equals(resp.getResponsibilityId()) && ObjectUtils.equals(request.getQualifiedRoleName(), qualifiedRoleName))
440             {
441                 return true;
442             }
443         }
444 		return false;
445 	}
446 
447 	public RuleService getRuleService() {
448 		return KEWServiceLocator.getRuleService();
449 	}
450 
451 	public RuleDelegationService getRuleDelegationService() {
452 		return KEWServiceLocator.getRuleDelegationService();
453 	}
454 
455 	
456 	private ActionRequestService getActionRequestService() {
457 		return KEWServiceLocator.getActionRequestService();
458 	}
459 
460 	public int getNumberOfMatchingRules() {
461 		return selectedRules;
462 	}
463 
464 	//  private DocumentContent parseDocumentContent(RouteContext context) throws WorkflowException {
465 	//  try {
466 	//  return new StandardDocumentContent(context.getDocument().getDocContent(), context);
467 	//  } catch (Exception e) {
468 	//  throw new WorkflowException("Error parsing doc content for document " + context.getDocument().getRouteHeaderId());
469 	//  }
470 	//  }
471 }