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                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}