001    /**
002     * Copyright 2005-2012 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.xml;
017    
018    import org.apache.commons.lang.StringUtils;
019    import org.jdom.Document;
020    import org.jdom.Element;
021    import org.jdom.JDOMException;
022    import org.kuali.rice.core.api.delegation.DelegationType;
023    import org.kuali.rice.core.api.util.RiceConstants;
024    import org.kuali.rice.core.api.util.xml.XmlException;
025    import org.kuali.rice.core.api.util.xml.XmlHelper;
026    import org.kuali.rice.kew.api.action.ActionRequestPolicy;
027    import org.kuali.rice.kew.api.rule.RoleName;
028    import org.kuali.rice.kew.doctype.bo.DocumentType;
029    import org.kuali.rice.kew.rule.RuleBaseValues;
030    import org.kuali.rice.kew.rule.RuleDelegationBo;
031    import org.kuali.rice.kew.rule.RuleExpressionDef;
032    import org.kuali.rice.kew.rule.RuleResponsibilityBo;
033    import org.kuali.rice.kew.rule.bo.RuleTemplateBo;
034    import org.kuali.rice.kew.service.KEWServiceLocator;
035    import org.kuali.rice.kew.api.KewApiConstants;
036    import org.kuali.rice.kew.util.Utilities;
037    import org.kuali.rice.kim.api.group.Group;
038    import org.kuali.rice.kim.api.identity.principal.Principal;
039    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
040    import org.xml.sax.SAXException;
041    
042    import javax.xml.parsers.ParserConfigurationException;
043    import java.io.IOException;
044    import java.io.InputStream;
045    import java.sql.Timestamp;
046    import java.text.ParseException;
047    import java.util.ArrayList;
048    import java.util.Iterator;
049    import java.util.List;
050    
051    import 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     */
060    public 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    }