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    }