001/**
002 * Copyright 2005-2015 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.xml;
017
018import org.apache.commons.lang.StringUtils;
019import org.jdom.Document;
020import org.jdom.Element;
021import org.jdom.JDOMException;
022import org.kuali.rice.core.api.delegation.DelegationType;
023import org.kuali.rice.core.api.util.RiceConstants;
024import org.kuali.rice.core.api.util.xml.XmlException;
025import org.kuali.rice.core.api.util.xml.XmlHelper;
026import org.kuali.rice.kew.api.action.ActionRequestPolicy;
027import org.kuali.rice.kew.api.rule.RoleName;
028import org.kuali.rice.kew.doctype.bo.DocumentType;
029import org.kuali.rice.kew.rule.RuleBaseValues;
030import org.kuali.rice.kew.rule.RuleDelegationBo;
031import org.kuali.rice.kew.rule.RuleExpressionDef;
032import org.kuali.rice.kew.rule.RuleResponsibilityBo;
033import org.kuali.rice.kew.rule.bo.RuleTemplateBo;
034import org.kuali.rice.kew.service.KEWServiceLocator;
035import org.kuali.rice.kew.api.KewApiConstants;
036import org.kuali.rice.kew.util.Utilities;
037import org.kuali.rice.kim.api.group.Group;
038import org.kuali.rice.kim.api.identity.principal.Principal;
039import org.kuali.rice.kim.api.services.KimApiServiceLocator;
040import org.xml.sax.SAXException;
041
042import javax.xml.parsers.ParserConfigurationException;
043import java.io.IOException;
044import java.io.InputStream;
045import java.sql.Timestamp;
046import java.text.ParseException;
047import java.util.ArrayList;
048import java.util.Iterator;
049import java.util.List;
050
051import static org.kuali.rice.core.api.impex.xml.XmlConstants.*;
052
053/**
054 * Parses rules from XML.
055 *
056 * @see RuleBaseValues
057 *
058 * @author Kuali Rice Team (rice.collab@kuali.org)
059 */
060public class RuleXmlParser {
061
062    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(RuleXmlParser.class);
063
064    /**
065     * Priority to use if rule responsibility omits priority
066     */
067    private static final int DEFAULT_RULE_PRIORITY = 1;
068    /**
069     * Value of Force Action flag if omitted; default to false, we will NOT force action for approvals
070     */
071    private static final boolean DEFAULT_FORCE_ACTION = false;
072    /**
073     * Default approve policy, if omitted; defaults to FIRST_APPROVE, the request will be satisfied by the first approval
074     */
075    private static final String DEFAULT_APPROVE_POLICY = ActionRequestPolicy.FIRST.getCode();
076    /**
077     * Default action requested, if omitted; defaults to "A"pprove
078     */
079    private static final String DEFAULT_ACTION_REQUESTED = KewApiConstants.ACTION_REQUEST_APPROVE_REQ;
080
081    public List<RuleDelegationBo> parseRuleDelegations(InputStream input) throws IOException, XmlException {
082        try {
083            Document doc = XmlHelper.trimSAXXml(input);
084            Element root = doc.getRootElement();
085            return parseRuleDelegations(root);
086        } catch (JDOMException e) {
087            throw new XmlException("Parse error.", e);
088        } catch (SAXException e){
089            throw new XmlException("Parse error.",e);
090        } catch(ParserConfigurationException e){
091            throw new XmlException("Parse error.",e);
092        }
093    }
094    
095    public List<RuleBaseValues> parseRules(InputStream input) throws IOException, XmlException {
096        try {
097            Document doc = XmlHelper.trimSAXXml(input);
098            Element root = doc.getRootElement();
099            return parseRules(root);
100        } catch (JDOMException e) {
101            throw new XmlException("Parse error.", e);
102        } catch (SAXException e){
103            throw new XmlException("Parse error.",e);
104        } catch(ParserConfigurationException e){
105            throw new XmlException("Parse error.",e);
106        }
107    }
108
109    /**
110     * Parses and saves rules
111     * @param element top-level 'data' element which should contain a <rules> child element
112     * @throws XmlException
113     */
114    public List<RuleBaseValues> parseRules(Element element) throws XmlException {
115        List<RuleBaseValues> rulesToSave = new ArrayList<RuleBaseValues>();
116        for (Element rulesElement: (List<Element>) element.getChildren(RULES, RULE_NAMESPACE)) {
117            for (Element ruleElement: (List<Element>) rulesElement.getChildren(RULE, RULE_NAMESPACE)) {
118                RuleBaseValues rule = parseRule(ruleElement);
119                rulesToSave.add(rule);
120            }
121        }
122        checkForDuplicateRules(rulesToSave);
123        return KEWServiceLocator.getRuleService().saveRules(rulesToSave, false);
124    }
125    
126    /**
127     * Parses and saves rule delegations
128     * @param element top-level 'data' element which should contain a <rules> child element
129     * @throws XmlException
130     */
131    public List<RuleDelegationBo> parseRuleDelegations(Element element) throws XmlException {
132        List<RuleDelegationBo> ruleDelegationsToSave = new ArrayList<RuleDelegationBo>();
133        for (Element ruleDelegationsElement: (List<Element>) element.getChildren(RULE_DELEGATIONS, RULE_NAMESPACE)) {
134            for (Element ruleDelegationElement: (List<Element>) ruleDelegationsElement.getChildren(RULE_DELEGATION, RULE_NAMESPACE)) {
135                RuleDelegationBo ruleDelegation = parseRuleDelegation(ruleDelegationElement);
136                ruleDelegationsToSave.add(ruleDelegation);
137            }
138        }
139        //checkForDuplicateRuleDelegations(ruleDelegationsToSave);
140        return KEWServiceLocator.getRuleService().saveRuleDelegations(ruleDelegationsToSave, false);
141    }
142    
143    /**
144     * Checks for rules in the List that duplicate other Rules already in the system 
145     */
146    private void checkForDuplicateRules(List<RuleBaseValues> rules) throws XmlException {
147        for (RuleBaseValues rule : rules) {
148                if (StringUtils.isBlank(rule.getName())) {
149                        LOG.debug("Checking for rule duplication on an anonymous rule.");
150                        checkRuleForDuplicate(rule);
151                }
152        }
153    }
154    
155    /**
156     * Checks for rule delegations in the List that duplicate other Rules already in the system 
157     */
158    private void checkForDuplicateRuleDelegations(List<RuleDelegationBo> ruleDelegations) throws XmlException {
159        for (RuleDelegationBo ruleDelegation : ruleDelegations) {
160                if (StringUtils.isBlank(ruleDelegation.getDelegationRule().getName())) {
161                        LOG.debug("Checking for rule duplication on an anonymous rule delegation.");
162                        checkRuleDelegationForDuplicate(ruleDelegation);
163                }
164        }
165    }
166
167    private RuleDelegationBo parseRuleDelegation(Element element) throws XmlException {
168        RuleDelegationBo ruleDelegation = new RuleDelegationBo();
169        Element parentResponsibilityElement = element.getChild(PARENT_RESPONSIBILITY, element.getNamespace());
170        if (parentResponsibilityElement == null) {
171                throw new XmlException("parent responsibility was not defined");
172        }
173        String parentResponsibilityId = parseParentResponsibilityId(parentResponsibilityElement);
174        String delegationType = element.getChildText(DELEGATION_TYPE, element.getNamespace());
175        if (delegationType == null || DelegationType.parseCode(delegationType) == null) {
176            throw new XmlException("Invalid delegation type specified for delegate rule '" + delegationType + "'");
177        }
178        
179        ruleDelegation.setResponsibilityId(parentResponsibilityId);
180        ruleDelegation.setDelegationType(DelegationType.fromCode(delegationType));
181        
182        Element ruleElement = element.getChild(RULE, element.getNamespace());
183        RuleBaseValues rule = parseRule(ruleElement);
184        rule.setDelegateRule(true);
185        ruleDelegation.setDelegationRule(rule);
186        
187        return ruleDelegation;
188    }
189    
190    private String parseParentResponsibilityId(Element element) throws XmlException {
191        String responsibilityId = element.getChildText(RESPONSIBILITY_ID, element.getNamespace());
192        if (!StringUtils.isBlank(responsibilityId)) {
193                return responsibilityId;
194        }
195        String parentRuleName = element.getChildText(PARENT_RULE_NAME, element.getNamespace());
196        if (StringUtils.isBlank(parentRuleName)) {
197                throw new XmlException("One of responsibilityId or parentRuleName needs to be defined");
198        }
199        RuleBaseValues parentRule = KEWServiceLocator.getRuleService().getRuleByName(parentRuleName);
200        if (parentRule == null) {
201                throw new XmlException("Could find the parent rule with name '" + parentRuleName + "'");
202        }
203        RuleResponsibilityBo ruleResponsibilityNameAndType = CommonXmlParser.parseResponsibilityNameAndType(element);
204        if (ruleResponsibilityNameAndType == null) {
205                throw new XmlException("Could not locate a valid responsibility declaration for the parent responsibility.");
206        }
207        String parentResponsibilityId = KEWServiceLocator.getRuleService().findResponsibilityIdForRule(parentRuleName, 
208                        ruleResponsibilityNameAndType.getRuleResponsibilityName(),
209                        ruleResponsibilityNameAndType.getRuleResponsibilityType());
210        if (parentResponsibilityId == null) {
211                throw new XmlException("Failed to locate parent responsibility for rule with name '" + parentRuleName + "' and responsibility " + ruleResponsibilityNameAndType);
212        }
213        return parentResponsibilityId;
214    }
215    
216    /**
217     * Parses, and only parses, a rule definition (be it a top-level rule, or a rule delegation).  This method will
218     * NOT dirty or save any existing data, it is side-effect-free.
219     * @param element the rule element
220     * @return a new RuleBaseValues object which is not yet saved
221     * @throws XmlException
222     */
223    private RuleBaseValues parseRule(Element element) throws XmlException {
224        String name = element.getChildText(NAME, element.getNamespace());
225        RuleBaseValues rule = createRule(name);
226        
227        setDefaultRuleValues(rule);
228        rule.setName(name);
229        
230        String toDatestr = element.getChildText( TO_DATE, element.getNamespace());
231        String fromDatestr = element.getChildText( FROM_DATE, element.getNamespace());
232        rule.setToDateValue(formatDate("toDate", toDatestr));
233        rule.setFromDateValue(formatDate("fromDate", fromDatestr));
234
235        String description = element.getChildText(DESCRIPTION, element.getNamespace());
236        if (StringUtils.isBlank(description)) {
237            throw new XmlException("Rule must have a description.");
238        }
239                
240        String documentTypeName = element.getChildText(DOCUMENT_TYPE, element.getNamespace());
241        if (StringUtils.isBlank(documentTypeName)) {
242                throw new XmlException("Rule must have a document type.");
243        }
244        DocumentType documentType = KEWServiceLocator.getDocumentTypeService().findByName(documentTypeName);
245        if (documentType == null) {
246                throw new XmlException("Could not locate document type '" + documentTypeName + "'");
247        }
248
249        RuleTemplateBo ruleTemplate = null;
250        String ruleTemplateName = element.getChildText(RULE_TEMPLATE, element.getNamespace());        
251        Element ruleExtensionsElement = element.getChild(RULE_EXTENSIONS, element.getNamespace());
252        if (!StringUtils.isBlank(ruleTemplateName)) {
253                ruleTemplate = KEWServiceLocator.getRuleTemplateService().findByRuleTemplateName(ruleTemplateName);
254                if (ruleTemplate == null) {
255                        throw new XmlException("Could not locate rule template '" + ruleTemplateName + "'");
256                }
257        } else {
258                if (ruleExtensionsElement != null) {
259                        throw new XmlException("Templateless rules may not have rule extensions");
260                }
261        }
262
263        RuleExpressionDef ruleExpressionDef = null;
264        Element exprElement = element.getChild(RULE_EXPRESSION, element.getNamespace());
265        if (exprElement != null) {
266                String exprType = exprElement.getAttributeValue("type");
267                if (StringUtils.isEmpty(exprType)) {
268                        throw new XmlException("Expression type must be specified");
269                }
270                String expression = exprElement.getTextTrim();
271                ruleExpressionDef = new RuleExpressionDef();
272                ruleExpressionDef.setType(exprType);
273                ruleExpressionDef.setExpression(expression);
274        }
275        
276        String forceActionValue = element.getChildText(FORCE_ACTION, element.getNamespace());
277        Boolean forceAction = Boolean.valueOf(DEFAULT_FORCE_ACTION);
278        if (!StringUtils.isBlank(forceActionValue)) {
279            forceAction = Boolean.valueOf(forceActionValue);
280        }
281
282        rule.setDocTypeName(documentType.getName());
283        if (ruleTemplate != null) {
284            rule.setRuleTemplateId(ruleTemplate.getId());
285            rule.setRuleTemplate(ruleTemplate);
286        }
287        if (ruleExpressionDef != null) {
288            rule.setRuleExpressionDef(ruleExpressionDef);
289        }
290        rule.setDescription(description);
291        rule.setForceAction(forceAction);
292
293        Element responsibilitiesElement = element.getChild(RESPONSIBILITIES, element.getNamespace());
294        rule.setRuleResponsibilities(parseResponsibilities(responsibilitiesElement, rule));
295        rule.setRuleExtensions(parseRuleExtensions(ruleExtensionsElement, rule));
296
297        return rule;
298    }
299    
300    /**
301     * Creates the rule that the parser will populate.  If a rule with the given name
302     * already exists, it's keys and responsibilities will be copied over to the
303     * new rule.  The calling code will then sort through the original responsibilities
304     * and compare them with those being defined on the XML being parsed.
305     */
306    private RuleBaseValues createRule(String ruleName) {
307        RuleBaseValues rule = new RuleBaseValues();
308        RuleBaseValues existingRule = (ruleName != null) ? KEWServiceLocator.getRuleService().getRuleByName(ruleName) : null;
309        if (existingRule != null) {
310                // copy keys and responsibiliities from the existing rule
311                rule.setId(existingRule.getId());
312                rule.setPreviousRuleId(existingRule.getPreviousRuleId());
313                rule.setPreviousVersion(existingRule.getPreviousVersion());
314                rule.setRuleResponsibilities(existingRule.getRuleResponsibilities());
315        }
316        return rule;
317    }
318
319    /**
320     * Checks to see whether this anonymous rule duplicates an existing rule.
321     * Currently the uniqueness is on ruleResponsibilityName, and extension key/values.
322     * @param rule the rule to check
323     * @throws XmlException if this incoming rule duplicates an existing rule
324     */
325    private void checkRuleForDuplicate(RuleBaseValues rule) throws XmlException {
326        String ruleId = KEWServiceLocator.getRuleService().getDuplicateRuleId(rule);
327        if (ruleId != null) {
328                throw new XmlException("Rule '" + rule.getDescription() + "' on doc '" + rule.getDocTypeName() + "' is a duplicate of rule with rule Id " + ruleId);
329        }
330    }
331    
332    private void checkRuleDelegationForDuplicate(RuleDelegationBo ruleDelegation) throws XmlException {
333        checkRuleForDuplicate(ruleDelegation.getDelegationRule());
334    }
335
336    private void setDefaultRuleValues(RuleBaseValues rule) throws XmlException {
337        rule.setForceAction(Boolean.FALSE);
338        rule.setActivationDate(new Timestamp(System.currentTimeMillis()));
339        rule.setActive(Boolean.TRUE);
340        rule.setCurrentInd(Boolean.TRUE);
341        rule.setTemplateRuleInd(Boolean.FALSE);
342        rule.setVersionNbr(new Integer(0));
343        rule.setDelegateRule(false);
344    }
345
346    private List<RuleResponsibilityBo> parseResponsibilities(Element element, RuleBaseValues rule) throws XmlException {
347        if (element == null) {
348            return new ArrayList<RuleResponsibilityBo>(0);
349        }
350        List<RuleResponsibilityBo> existingResponsibilities = rule.getRuleResponsibilities();
351        List<RuleResponsibilityBo> responsibilities = new ArrayList<RuleResponsibilityBo>();
352        List<Element> responsibilityElements = (List<Element>)element.getChildren(RESPONSIBILITY, element.getNamespace());
353        for (Element responsibilityElement : responsibilityElements) {
354            RuleResponsibilityBo responsibility = parseResponsibility(responsibilityElement, rule);
355            reconcileWithExistingResponsibility(responsibility, existingResponsibilities);
356            responsibilities.add(responsibility);
357        }
358        if (responsibilities.size() == 0) {
359            throw new XmlException("Rule responsibility list must have at least one responsibility.");
360        }
361        return responsibilities;
362    }
363
364    public RuleResponsibilityBo parseResponsibility(Element element, RuleBaseValues rule) throws XmlException {
365        RuleResponsibilityBo responsibility = new RuleResponsibilityBo();
366        responsibility.setRuleBaseValues(rule);
367        String actionRequested = null;
368        String priority = null;
369        actionRequested = element.getChildText(ACTION_REQUESTED, element.getNamespace());
370        if (StringUtils.isBlank(actionRequested)) {
371                actionRequested = DEFAULT_ACTION_REQUESTED;
372        }
373        priority = element.getChildText(PRIORITY, element.getNamespace());
374        if (StringUtils.isBlank(priority)) {
375                priority = String.valueOf(DEFAULT_RULE_PRIORITY);
376        }
377        String approvePolicy = element.getChildText(APPROVE_POLICY, element.getNamespace());
378        Element delegations = element.getChild(DELEGATIONS, element.getNamespace());
379        if (actionRequested == null) {
380            throw new XmlException("actionRequested is required on responsibility");
381        }
382        if (!actionRequested.equals(KewApiConstants.ACTION_REQUEST_COMPLETE_REQ) && !actionRequested.equals(KewApiConstants.ACTION_REQUEST_APPROVE_REQ) && !actionRequested.equals(KewApiConstants.ACTION_REQUEST_ACKNOWLEDGE_REQ) && !actionRequested.equals(KewApiConstants.ACTION_REQUEST_FYI_REQ)) {
383            throw new XmlException("Invalid action requested code '" + actionRequested + "'");
384        }
385        if (StringUtils.isBlank(approvePolicy)) {
386            approvePolicy = DEFAULT_APPROVE_POLICY;
387        }
388        if (!approvePolicy.equals(ActionRequestPolicy.ALL.getCode()) && !approvePolicy.equals(ActionRequestPolicy.FIRST.getCode())) {
389            throw new XmlException("Invalid approve policy '" + approvePolicy + "'");
390        }
391        Integer priorityNumber = Integer.valueOf(priority);
392        responsibility.setActionRequestedCd(actionRequested);
393        responsibility.setPriority(priorityNumber);
394        responsibility.setApprovePolicy(approvePolicy);
395        
396        RuleResponsibilityBo responsibilityNameAndType = CommonXmlParser.parseResponsibilityNameAndType(element);
397        if (responsibilityNameAndType == null) {
398                throw new XmlException("Could not locate a valid responsibility declaration on a responsibility on rule with description '" + rule.getDescription() + "'");
399        }
400        if (responsibilityNameAndType.getRuleResponsibilityType().equals(KewApiConstants.RULE_RESPONSIBILITY_GROUP_ID)
401                        && responsibility.getApprovePolicy().equals(ActionRequestPolicy.ALL.getCode())) {
402                throw new XmlException("Invalid approve policy '" + approvePolicy + "'.  This policy is not supported with Groups.");
403        }
404        responsibility.setRuleResponsibilityName(responsibilityNameAndType.getRuleResponsibilityName());
405        responsibility.setRuleResponsibilityType(responsibilityNameAndType.getRuleResponsibilityType());
406        
407        return responsibility;
408    }
409    
410    /**
411     * Attempts to reconcile the given RuleResponsibility with the list of existing responsibilities (in the case of a
412     * rule being updated via the XML).  This goal of this code is to copy responsibility ids from existing responsibilities
413     * to the new responsibility where appropriate.  The code will attempt to find exact matches based on the values found
414     * on the responsibilities.
415     */
416    private void reconcileWithExistingResponsibility(RuleResponsibilityBo responsibility, List<RuleResponsibilityBo> existingResponsibilities) {
417        if (existingResponsibilities == null || existingResponsibilities.isEmpty()) {
418                return;
419        }
420        RuleResponsibilityBo exactMatch = null;
421        for (RuleResponsibilityBo existingResponsibility : existingResponsibilities) {
422                if (isExactResponsibilityMatch(responsibility, existingResponsibility)) {
423                        exactMatch = existingResponsibility;
424                        break;
425                }
426        }
427        if (exactMatch != null) {
428                responsibility.setResponsibilityId(exactMatch.getResponsibilityId());
429        }
430    }
431    
432    /**
433     * Checks if the given responsibilities are exact matches of one another.
434     */
435    private boolean isExactResponsibilityMatch(RuleResponsibilityBo newResponsibility, RuleResponsibilityBo existingResponsibility) {
436        if (existingResponsibility.getResponsibilityId().equals(newResponsibility.getResponsibilityId())) {
437                return true;
438        }
439        if (existingResponsibility.getRuleResponsibilityName().equals(newResponsibility.getRuleResponsibilityName()) &&
440                        existingResponsibility.getRuleResponsibilityType().equals(newResponsibility.getRuleResponsibilityType()) &&
441                        existingResponsibility.getApprovePolicy().equals(newResponsibility.getApprovePolicy()) &&
442                        existingResponsibility.getActionRequestedCd().equals(newResponsibility.getActionRequestedCd()) &&
443                        existingResponsibility.getPriority().equals(newResponsibility.getPriority())) {
444                return true;
445        }
446        return false;
447    }
448
449    private List parseRuleExtensions(Element element, RuleBaseValues rule) throws XmlException {
450        if (element == null) {
451            return new ArrayList();
452        }
453        RuleExtensionXmlParser parser = new RuleExtensionXmlParser();
454        return parser.parseRuleExtensions(element, rule);
455    }
456    
457    public Timestamp formatDate(String dateLabel, String dateString) throws XmlException {
458        if (StringUtils.isBlank(dateString)) {
459                return null;
460        }
461        try {
462                return new Timestamp(RiceConstants.getDefaultDateFormat().parse(dateString).getTime());
463        } catch (ParseException e) {
464                throw new XmlException(dateLabel + " is not in the proper format.  Should have been: " + RiceConstants.DEFAULT_DATE_FORMAT_PATTERN);
465        }
466    }
467    
468}