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