001 /** 002 * Copyright 2005-2013 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 }