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.krad.service.impl;
017
018import org.apache.commons.lang.StringUtils;
019import org.apache.log4j.Logger;
020import org.kuali.rice.core.api.exception.RiceRuntimeException;
021import org.kuali.rice.krad.bo.AdHocRoutePerson;
022import org.kuali.rice.krad.bo.AdHocRouteWorkgroup;
023import org.kuali.rice.krad.document.Document;
024import org.kuali.rice.krad.document.TransactionalDocument;
025import org.kuali.rice.krad.maintenance.MaintenanceDocument;
026import org.kuali.rice.krad.rules.MaintenanceDocumentRuleBase;
027import org.kuali.rice.krad.rules.TransactionalDocumentRuleBase;
028import org.kuali.rice.krad.rules.rule.BusinessRule;
029import org.kuali.rice.krad.rules.rule.event.AddAdHocRoutePersonEvent;
030import org.kuali.rice.krad.rules.rule.event.AddAdHocRouteWorkgroupEvent;
031import org.kuali.rice.krad.rules.rule.event.DocumentEvent;
032import org.kuali.rice.krad.rules.rule.event.RuleEvent;
033import org.kuali.rice.krad.service.DataDictionaryService;
034import org.kuali.rice.krad.service.DocumentDictionaryService;
035import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
036import org.kuali.rice.krad.service.KualiRuleService;
037import org.kuali.rice.krad.uif.component.MethodInvokerConfig;
038import org.kuali.rice.krad.util.GlobalVariables;
039import org.kuali.rice.krad.util.KRADConstants;
040import org.kuali.rice.krad.util.MessageMap;
041
042import java.util.ArrayList;
043import java.util.List;
044
045/**
046 * Represents a rule evaluator for Kuali. This class is to be used for evaluating business rule checks. The class
047 * defines one method right now - applyRules() which takes in a Document and a DocumentEvent and does the proper
048 * business rule checks based on the context of the event and the document type
049 *
050 * @author Kuali Rice Team (rice.collab@kuali.org)
051 */
052public class KualiRuleServiceImpl implements KualiRuleService {
053    private static final Logger LOG = Logger.getLogger(KualiRuleServiceImpl.class);
054
055    private DocumentDictionaryService documentDictionaryService;
056    private DataDictionaryService dataDictionaryService;
057
058    /**
059     * @see org.kuali.rice.krad.service.KualiRuleService#applyRules(org.kuali.rice.krad.rules.rule.event.DocumentEvent)
060     */
061    public boolean applyRules(DocumentEvent event) {
062        if (event == null) {
063            throw new IllegalArgumentException("invalid (null) event");
064        }
065
066        event.validate();
067        if (LOG.isDebugEnabled()) {
068            LOG.debug("calling applyRules for event " + event);
069        }
070
071        BusinessRule rule = getBusinessRulesInstance(event.getDocument(), event.getRuleInterfaceClass());
072
073        boolean success = true;
074        if (rule != null) {
075            if (LOG.isDebugEnabled()) {
076                LOG.debug("processing " + event.getName() + " with rule " + rule.getClass().getName());
077            }
078            increaseErrorPath(event.getErrorPathPrefix());
079
080            // get any child events and apply rules
081            List<RuleEvent> events = event.generateEvents();
082            for (RuleEvent generatedEvent : events) {
083                success &= applyRules((DocumentEvent) generatedEvent);
084            }
085
086            // now call the event rule method
087            if (StringUtils.isNotBlank(event.getRuleMethodName())) {
088                success &= invokeBusinessRuleMethod(rule, event);
089            } else {
090                success &= event.invokeRuleMethod(rule);
091            }
092
093            decreaseErrorPath(event.getErrorPathPrefix());
094
095            // report failures
096            if (!success) {
097                if (LOG.isDebugEnabled()) { // NO, this is not a type - only log if in debug mode - this is not an error in production
098                    LOG.debug(event.getName() + " businessRule " + rule.getClass().getName() + " failed");
099                }
100            } else {
101                if (LOG.isDebugEnabled()) {
102                    LOG.debug("processed " + event.getName() + " for rule " + rule.getClass().getName());
103                }
104            }
105
106        }
107        return success;
108    }
109
110    /**
111     * local helper method to invoke the business rule method
112     *
113     * @param rule the business rule class that the method to invoke belongs to
114     * @param event the document event the rule applies to
115     * @return a boolean to indicate whether the method invocation was a succes or not
116     */
117    public boolean invokeBusinessRuleMethod(BusinessRule rule, DocumentEvent event) {
118        boolean success = true;
119
120        String methodName = event.getRuleMethodName();
121        MethodInvokerConfig methodInvoker = new MethodInvokerConfig();
122        methodInvoker.setTargetClass(rule.getClass());
123        methodInvoker.setTargetMethod(methodName);
124
125        Object[] arguments = new Object[1];
126        arguments[0] = event;
127        methodInvoker.setArguments(arguments);
128
129        // invoke rule method
130        try {
131            LOG.debug("Invoking rule method: " + methodInvoker.getTargetMethod() + " for class: " + rule.getClass()
132                    .getName());
133            methodInvoker.prepare();
134
135            Class<?> methodReturnType = methodInvoker.getPreparedMethod().getReturnType();
136            if (StringUtils.equals("void", methodReturnType.getName())) {
137                methodInvoker.invoke();
138            } else {
139                success &= (Boolean) methodInvoker.invoke();
140            }
141        } catch (Exception e) {
142            LOG.error("Error invoking rule method for class: " + rule.getClass().getName(), e);
143            throw new RuntimeException("Error invoking rule method for class: " + rule.getClass().getName(), e);
144        }
145
146        return success;
147    }
148
149    /**
150     * Builds a list containing AddAdHocRoutePersonEvents since the validation done for an AdHocRouteRecipient is the
151     * same for all events
152     *
153     * @see org.kuali.rice.krad.service.KualiRuleService#generateAdHocRoutePersonEvents(org.kuali.rice.krad.document.Document)
154     */
155    public List<AddAdHocRoutePersonEvent> generateAdHocRoutePersonEvents(Document document) {
156        List<AdHocRoutePerson> adHocRoutePersons = document.getAdHocRoutePersons();
157
158        List<AddAdHocRoutePersonEvent> events = new ArrayList<AddAdHocRoutePersonEvent>();
159
160        for (int i = 0; i < adHocRoutePersons.size(); i++) {
161            events.add(new AddAdHocRoutePersonEvent(
162                    KRADConstants.EXISTING_AD_HOC_ROUTE_PERSON_PROPERTY_NAME + "[" + i + "]", document,
163                    adHocRoutePersons.get(i)));
164        }
165
166        return events;
167    }
168
169    /**
170     * Builds a list containing AddAdHocRoutePersonEvents since the validation done for an AdHocRouteRecipient is the
171     * same for all events
172     *
173     * @see org.kuali.rice.krad.service.KualiRuleService#generateAdHocRouteWorkgroupEvents(org.kuali.rice.krad.document.Document)
174     */
175    public List<AddAdHocRouteWorkgroupEvent> generateAdHocRouteWorkgroupEvents(Document document) {
176        List<AdHocRouteWorkgroup> adHocRouteWorkgroups = document.getAdHocRouteWorkgroups();
177
178        List<AddAdHocRouteWorkgroupEvent> events = new ArrayList<AddAdHocRouteWorkgroupEvent>();
179
180        for (int i = 0; i < adHocRouteWorkgroups.size(); i++) {
181            events.add(new AddAdHocRouteWorkgroupEvent(
182                    KRADConstants.EXISTING_AD_HOC_ROUTE_WORKGROUP_PROPERTY_NAME + "[" + i + "]", document,
183                    adHocRouteWorkgroups.get(i)));
184        }
185
186        return events;
187    }
188
189    /**
190     * @param document
191     * @param ruleInterface
192     * @return instance of the businessRulesClass for the given document's type, if that businessRulesClass implements
193     * the given ruleInterface
194     */
195    public BusinessRule getBusinessRulesInstance(Document document, Class<? extends BusinessRule> ruleInterface) {
196        // get the businessRulesClass
197        Class<? extends BusinessRule> businessRulesClass = null;
198        if (document instanceof TransactionalDocument) {
199            TransactionalDocument transactionalDocument = (TransactionalDocument) document;
200
201            businessRulesClass = getDocumentDictionaryService().getBusinessRulesClass(transactionalDocument);
202            if (businessRulesClass == null) {
203                return new TransactionalDocumentRuleBase(); // default to a generic rule that will enforce Required fields
204            }
205        } else if (document instanceof MaintenanceDocument) {
206            MaintenanceDocument maintenanceDocument = (MaintenanceDocument) document;
207
208            businessRulesClass = getDocumentDictionaryService().getBusinessRulesClass(maintenanceDocument);
209            if (businessRulesClass == null) {
210                return new MaintenanceDocumentRuleBase(); // default to a generic rule that will enforce Required fields
211            }
212        } else {
213            LOG.error("unable to get businessRulesClass for unknown document type '"
214                    + document.getClass().getName()
215                    + "'");
216        }
217
218        // instantiate and return it if it implements the given ruleInterface
219        BusinessRule rule = null;
220        if (businessRulesClass != null) {
221            try {
222                if (ruleInterface.isAssignableFrom(businessRulesClass)) {
223                    rule = businessRulesClass.newInstance();
224                }
225            } catch (IllegalAccessException e) {
226                throw new RiceRuntimeException("error processing business rules", e);
227            } catch (InstantiationException e) {
228                throw new RiceRuntimeException("error processing business rules", e);
229            }
230        }
231
232        return rule;
233    }
234
235    /**
236     * Increases the registered error path, so that field highlighting can occur on the appropriate object
237     * attribute
238     *
239     * @param errorPathPrefix
240     */
241    private void increaseErrorPath(String errorPathPrefix) {
242        MessageMap errorMap = GlobalVariables.getMessageMap();
243
244        if (!StringUtils.isBlank(errorPathPrefix)) {
245            errorMap.addToErrorPath(errorPathPrefix);
246        }
247    }
248
249    /**
250     * Decreases the registered error path, so that field highlighting can occur on the appropriate object
251     * attribute
252     *
253     * @param errorPathPrefix
254     */
255    private void decreaseErrorPath(String errorPathPrefix) {
256        MessageMap errorMap = GlobalVariables.getMessageMap();
257
258        if (!StringUtils.isBlank(errorPathPrefix)) {
259            errorMap.removeFromErrorPath(errorPathPrefix);
260        }
261    }
262
263    public DocumentDictionaryService getDocumentDictionaryService() {
264        if (documentDictionaryService == null) {
265            this.documentDictionaryService = KRADServiceLocatorWeb.getDocumentDictionaryService();
266        }
267        return documentDictionaryService;
268    }
269
270    public void setDocumentDictionaryService(DocumentDictionaryService documentDictionaryService) {
271        this.documentDictionaryService = documentDictionaryService;
272    }
273
274    public DataDictionaryService getDataDictionaryService() {
275        return dataDictionaryService;
276    }
277
278    public void setDataDictionaryService(DataDictionaryService dataDictionaryService) {
279        this.dataDictionaryService = dataDictionaryService;
280    }
281}