001/** 002 * Copyright 2005-2016 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 */ 016package org.kuali.rice.kew.rule; 017 018import org.apache.commons.lang.ObjectUtils; 019import org.apache.commons.lang.StringUtils; 020import org.apache.log4j.Logger; 021import org.kuali.rice.core.api.exception.RiceRuntimeException; 022import org.kuali.rice.core.api.reflect.ObjectDefinition; 023import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader; 024import org.kuali.rice.core.api.util.ClassLoaderUtils; 025import org.kuali.rice.kew.actionrequest.ActionRequestFactory; 026import org.kuali.rice.kew.actionrequest.ActionRequestValue; 027import org.kuali.rice.kew.actionrequest.KimGroupRecipient; 028import org.kuali.rice.kew.actionrequest.KimPrincipalRecipient; 029import org.kuali.rice.kew.actionrequest.Recipient; 030import org.kuali.rice.kew.actionrequest.service.ActionRequestService; 031import org.kuali.rice.kew.api.KewApiServiceLocator; 032import org.kuali.rice.kew.api.action.ActionRequestStatus; 033import org.kuali.rice.kew.api.exception.WorkflowException; 034import org.kuali.rice.kew.api.extension.ExtensionDefinition; 035import org.kuali.rice.kew.api.rule.RuleDelegation; 036import org.kuali.rice.kew.api.rule.RuleResponsibility; 037import org.kuali.rice.kew.api.rule.RuleService; 038import org.kuali.rice.kew.api.rule.RuleTemplateAttribute; 039import org.kuali.rice.kew.engine.RouteContext; 040import org.kuali.rice.kew.engine.node.NodeState; 041import org.kuali.rice.kew.engine.node.RouteNode; 042import org.kuali.rice.kew.engine.node.RouteNodeInstance; 043import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue; 044import org.kuali.rice.kew.service.KEWServiceLocator; 045import org.kuali.rice.kew.user.RoleRecipient; 046import org.kuali.rice.kew.api.KewApiConstants; 047import org.kuali.rice.kew.util.PerformanceLogger; 048import org.kuali.rice.kew.util.ResponsibleParty; 049import org.kuali.rice.kew.util.Utilities; 050 051import java.sql.Timestamp; 052import java.util.ArrayList; 053import java.util.List; 054import 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 */ 067public 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 return null; 225 } 226 227 private void makeActionRequests(ActionRequestFactory arFactory, RouteContext context, org.kuali.rice.kew.api.rule.Rule rule, DocumentRouteHeaderValue routeHeader, ActionRequestValue parentRequest, RuleDelegation ruleDelegation) 228 throws WorkflowException { 229 230 List<org.kuali.rice.kew.api.rule.RuleResponsibility> responsibilities = rule.getRuleResponsibilities(); 231 makeActionRequests(arFactory, responsibilities, context, rule, routeHeader, parentRequest, ruleDelegation); 232 } 233 234 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) { 235 236 // Set actionRequests = new HashSet(); 237 for (org.kuali.rice.kew.api.rule.RuleResponsibility responsibility : responsibilities) 238 { 239 // arFactory = new ActionRequestFactory(routeHeader); 240 241 if (responsibility.isUsingRole()) 242 { 243 makeRoleActionRequests(arFactory, context, rule, responsibility, routeHeader, parentRequest, ruleDelegation); 244 } else 245 { 246 makeActionRequest(arFactory, context, rule, routeHeader, responsibility, parentRequest, ruleDelegation); 247 } 248 // if (arFactory.getRequestGraph() != null) { 249 // actionRequests.add(arFactory.getRequestGraph()); 250 // } 251 } 252 } 253 254 private void buildDelegationGraph(ActionRequestFactory arFactory, RouteContext context, 255 org.kuali.rice.kew.api.rule.Rule delegationRule, DocumentRouteHeaderValue routeHeaderValue, ActionRequestValue parentRequest, RuleDelegation ruleDelegation) { 256 context.setActionRequest(parentRequest); 257 RuleBaseValues delRuleBo = KEWServiceLocator.getRuleService().getRuleByName(delegationRule.getName()); 258 if (delegationRule.isActive()) { 259 for (org.kuali.rice.kew.api.rule.RuleResponsibility delegationResp : delegationRule.getRuleResponsibilities()) 260 { 261 if (delegationResp.isUsingRole()) 262 { 263 makeRoleActionRequests(arFactory, context, delegationRule, delegationResp, routeHeaderValue, parentRequest, ruleDelegation); 264 } else if (delRuleBo.isMatch(context.getDocumentContent())) 265 { 266 makeActionRequest(arFactory, context, delegationRule, routeHeaderValue, delegationResp, parentRequest, ruleDelegation); 267 } 268 } 269 } 270 } 271 272 /** 273 * Generates action requests for a role responsibility 274 */ 275 private void makeRoleActionRequests(ActionRequestFactory arFactory, RouteContext context, 276 org.kuali.rice.kew.api.rule.Rule rule, org.kuali.rice.kew.api.rule.RuleResponsibility resp, DocumentRouteHeaderValue routeHeader, ActionRequestValue parentRequest, 277 RuleDelegation ruleDelegation) 278 { 279 String roleName = resp.getResolvedRoleName(); 280 //RoleAttribute roleAttribute = resp.resolveRoleAttribute(); 281 RoleAttribute roleAttribute = null; 282 if (resp.isUsingRole()) { 283 //get correct extension definition 284 roleAttribute = (RoleAttribute) GlobalResourceLoader.getResourceLoader().getObject(new ObjectDefinition( 285 resp.getRoleAttributeName())); 286 287 if (roleAttribute instanceof XmlConfiguredAttribute) { 288 ExtensionDefinition roleAttributeDefinition = null; 289 for (RuleTemplateAttribute ruleTemplateAttribute : rule.getRuleTemplate().getRuleTemplateAttributes()) { 290 if (resp.getRoleAttributeName().equals(ruleTemplateAttribute.getRuleAttribute().getResourceDescriptor())) { 291 roleAttributeDefinition = ruleTemplateAttribute.getRuleAttribute(); 292 break; 293 } 294 } 295 ((XmlConfiguredAttribute)roleAttribute).setExtensionDefinition(roleAttributeDefinition); 296 } 297 } 298 //setRuleAttribute(roleAttribute, rule, resp.getRoleAttributeName()); 299 List<String> qualifiedRoleNames = new ArrayList<String>(); 300 if (parentRequest != null && parentRequest.getQualifiedRoleName() != null) { 301 qualifiedRoleNames.add(parentRequest.getQualifiedRoleName()); 302 } else { 303 qualifiedRoleNames.addAll(roleAttribute.getQualifiedRoleNames(roleName, context.getDocumentContent())); 304 } 305 for (String qualifiedRoleName : qualifiedRoleNames) { 306 if (parentRequest == null && isDuplicateActionRequestDetected(routeHeader, context.getNodeInstance(), resp, qualifiedRoleName)) { 307 continue; 308 } 309 310 ResolvedQualifiedRole resolvedRole = roleAttribute.resolveQualifiedRole(context, roleName, qualifiedRoleName); 311 RoleRecipient recipient = new RoleRecipient(roleName, qualifiedRoleName, resolvedRole); 312 if (parentRequest == null) { 313 ActionRequestValue roleRequest = arFactory.addRoleRequest(recipient, resp.getActionRequestedCd(), 314 resp.getApprovePolicy(), resp.getPriority(), resp.getResponsibilityId(), rule.isForceAction(), 315 rule.getDescription(), rule.getId()); 316 317 List<RuleDelegation> ruleDelegations = getRuleService().getRuleDelegationsByResponsibiltityId(resp.getResponsibilityId()); 318 if (ruleDelegations != null && !ruleDelegations.isEmpty()) { 319 // create delegations for all the children 320 for (ActionRequestValue request : roleRequest.getChildrenRequests()) { 321 for (RuleDelegation childRuleDelegation : ruleDelegations) { 322 buildDelegationGraph(arFactory, context, childRuleDelegation.getDelegationRule(), routeHeader, request, childRuleDelegation); 323 } 324 } 325 } 326 327 } else { 328 arFactory.addDelegationRoleRequest(parentRequest, resp.getApprovePolicy(), recipient, resp.getResponsibilityId(), rule.isForceAction(), ruleDelegation.getDelegationType(), rule.getDescription(), rule.getId()); 329 } 330 } 331 } 332 333 /** 334 * Determines if the attribute has a setRuleAttribute method and then sets the value appropriately if it does. 335 */ 336 /*private void setRuleAttribute(RoleAttribute roleAttribute, org.kuali.rice.kew.api.rule.Rule rule, String roleAttributeName) { 337 // look for a setRuleAttribute method on the RoleAttribute 338 Method setRuleAttributeMethod = null; 339 try { 340 setRuleAttributeMethod = roleAttribute.getClass().getMethod("setExtensionDefinition", RuleAttribute.class); 341 } catch (NoSuchMethodException e) { 342 LOG.info("method setRuleAttribute not found on " + RuleAttribute.class.getName()); 343 } 344 if (setRuleAttributeMethod == null) { 345 return; 346 } 347 // find the RuleAttribute by looking through the RuleTemplate 348 RuleTemplate ruleTemplate = rule.getRuleTemplate(); 349 if (ruleTemplate != null) { 350 for (RuleTemplateAttribute ruleTemplateAttribute : ruleTemplate.getActiveRuleTemplateAttributes()) 351 { 352 RuleAttribute ruleAttribute = ExtensionUtils.loadExtension(ruleTemplateAttribute.getRuleAttribute()); 353 if (ruleAttribute.getResourceDescriptor().equals(roleAttributeName)) 354 { 355 // this is our RuleAttribute! 356 try 357 { 358 setRuleAttributeMethod.invoke(roleAttribute, ruleAttribute); 359 break; 360 } catch (Exception e) 361 { 362 throw new WorkflowRuntimeException("Failed to set ExtensionDefinition on our RoleAttribute!", e); 363 } 364 } 365 } 366 } 367 }*/ 368 369 /** 370 * Generates action requests for a non-role responsibility, either a user or workgroup 371 * @throws org.kuali.rice.kew.api.exception.WorkflowException 372 */ 373 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, 374 RuleDelegation ruleDelegation) { 375 if (parentRequest == null && isDuplicateActionRequestDetected(routeHeader, context.getNodeInstance(), resp, null)) { 376 return; 377 } 378 Recipient recipient; 379 if (resp.isUsingPrincipal()) { 380 recipient = new KimPrincipalRecipient(resp.getPrincipalId()); 381 } else if (resp.isUsingGroup()) { 382 recipient = new KimGroupRecipient(resp.getGroupId()); 383 } else { 384 throw new RiceRuntimeException("Illegal rule responsibility type encountered"); 385 } 386 ActionRequestValue actionRequest; 387 if (parentRequest == null) { 388 actionRequest = arFactory.addRootActionRequest(resp.getActionRequestedCd(), 389 resp.getPriority(), 390 recipient, 391 rule.getDescription(), 392 resp.getResponsibilityId(), 393 rule.isForceAction(), 394 resp.getApprovePolicy(), 395 rule.getId()); 396 397 List<RuleDelegation> ruleDelegations = getRuleService().getRuleDelegationsByResponsibiltityId( 398 resp.getResponsibilityId()); 399 if (ruleDelegations != null && !ruleDelegations.isEmpty()) { 400 for (RuleDelegation childRuleDelegation : ruleDelegations) { 401 buildDelegationGraph(arFactory, context, childRuleDelegation.getDelegationRule(), routeHeader, actionRequest, childRuleDelegation); 402 } 403 } 404 405 } else { 406 arFactory.addDelegationRequest(parentRequest, recipient, resp.getResponsibilityId(), rule.isForceAction(), ruleDelegation.getDelegationType(), rule.getDescription(), rule.getId()); 407 } 408 } 409 410 private boolean isDuplicateActionRequestDetected(DocumentRouteHeaderValue routeHeader, RouteNodeInstance nodeInstance, org.kuali.rice.kew.api.rule.RuleResponsibility resp, String qualifiedRoleName) { 411 List<ActionRequestValue> requests = getActionRequestService().findByStatusAndDocId(ActionRequestStatus.DONE.getCode(), routeHeader.getDocumentId()); 412 for (ActionRequestValue request : requests) 413 { 414 if (((nodeInstance != null 415 && request.getNodeInstance() != null 416 && request.getNodeInstance().getRouteNodeInstanceId().equals(nodeInstance.getRouteNodeInstanceId()) 417 ) || request.getRouteLevel().equals(routeHeader.getDocRouteLevel()) 418 ) 419 && request.getResponsibilityId().equals(resp.getResponsibilityId()) 420 && ObjectUtils.equals(request.getQualifiedRoleName(), qualifiedRoleName)) { 421 return true; 422 } 423 } 424 return false; 425 } 426 427 public RuleService getRuleService() { 428 return KewApiServiceLocator.getRuleService(); 429 } 430 431 private ActionRequestService getActionRequestService() { 432 return KEWServiceLocator.getActionRequestService(); 433 } 434 435 public int getNumberOfMatchingRules() { 436 return selectedRules; 437 } 438 439}