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