001 /**
002 * Copyright 2005-2012 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016 package org.kuali.rice.kew.rule;
017
018 import org.apache.commons.lang.ObjectUtils;
019 import org.apache.commons.lang.StringUtils;
020 import org.apache.log4j.Logger;
021 import org.kuali.rice.core.api.exception.RiceRuntimeException;
022 import org.kuali.rice.core.api.reflect.ObjectDefinition;
023 import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
024 import org.kuali.rice.core.api.util.ClassLoaderUtils;
025 import org.kuali.rice.kew.actionrequest.ActionRequestFactory;
026 import org.kuali.rice.kew.actionrequest.ActionRequestValue;
027 import org.kuali.rice.kew.actionrequest.KimGroupRecipient;
028 import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient;
029 import org.kuali.rice.kew.actionrequest.Recipient;
030 import org.kuali.rice.kew.actionrequest.service.ActionRequestService;
031 import org.kuali.rice.kew.api.KewApiServiceLocator;
032 import org.kuali.rice.kew.api.action.ActionRequestStatus;
033 import org.kuali.rice.kew.api.exception.WorkflowException;
034 import org.kuali.rice.kew.api.extension.ExtensionDefinition;
035 import org.kuali.rice.kew.api.rule.RuleDelegation;
036 import org.kuali.rice.kew.api.rule.RuleResponsibility;
037 import org.kuali.rice.kew.api.rule.RuleService;
038 import org.kuali.rice.kew.api.rule.RuleTemplateAttribute;
039 import org.kuali.rice.kew.engine.RouteContext;
040 import org.kuali.rice.kew.engine.node.NodeState;
041 import org.kuali.rice.kew.engine.node.RouteNode;
042 import org.kuali.rice.kew.engine.node.RouteNodeInstance;
043 import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
044 import org.kuali.rice.kew.service.KEWServiceLocator;
045 import org.kuali.rice.kew.user.RoleRecipient;
046 import org.kuali.rice.kew.api.KewApiConstants;
047 import org.kuali.rice.kew.util.PerformanceLogger;
048 import org.kuali.rice.kew.util.ResponsibleParty;
049 import org.kuali.rice.kew.util.Utilities;
050
051 import java.sql.Timestamp;
052 import java.util.ArrayList;
053 import java.util.List;
054 import java.util.Map;
055
056
057 /**
058 * Generates Action Requests for a Document using the rule system and the specified
059 * {@link org.kuali.rice.kew.rule.bo.RuleTemplateBo}.
060 *
061 * @see ActionRequestValue
062 * @see org.kuali.rice.kew.rule.bo.RuleTemplateBo
063 * @see RuleBaseValues
064 *
065 * @author Kuali Rice Team (rice.collab@kuali.org)
066 */
067 public class FlexRM {
068
069 private static final Logger LOG = Logger.getLogger(FlexRM.class);
070
071 /**
072 * The default type of rule selector implementation to use if none is explicitly
073 * specified for the node.
074 */
075 public static final String DEFAULT_RULE_SELECTOR = "Template";
076 /**
077 * Package in which rule selector implementations live
078 */
079 private static final String RULE_SELECTOR_PACKAGE = "org.kuali.rice.kew.rule";
080 /**
081 * The class name suffix all rule selectors should have; e.g. FooRuleSelector
082 */
083 private static final String RULE_SELECTOR_SUFFIX= "RuleSelector";
084
085 private final Timestamp effectiveDate;
086 /**
087 * An accumulator that keeps track of the number of rules that have been selected over the lifespan of
088 * this FlexRM instance.
089 */
090 private int selectedRules;
091
092 public FlexRM() {
093 this.effectiveDate = null;
094 }
095
096 public FlexRM(Timestamp effectiveDate) {
097 this.effectiveDate = effectiveDate;
098 }
099
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 }