View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.xml;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.jdom.Document;
20  import org.jdom.Element;
21  import org.jdom.JDOMException;
22  import org.kuali.rice.core.api.delegation.DelegationType;
23  import org.kuali.rice.core.api.util.RiceConstants;
24  import org.kuali.rice.core.api.util.xml.XmlException;
25  import org.kuali.rice.core.api.util.xml.XmlHelper;
26  import org.kuali.rice.kew.api.action.ActionRequestPolicy;
27  import org.kuali.rice.kew.api.rule.RoleName;
28  import org.kuali.rice.kew.doctype.bo.DocumentType;
29  import org.kuali.rice.kew.rule.RuleBaseValues;
30  import org.kuali.rice.kew.rule.RuleDelegationBo;
31  import org.kuali.rice.kew.rule.RuleExpressionDef;
32  import org.kuali.rice.kew.rule.RuleResponsibilityBo;
33  import org.kuali.rice.kew.rule.bo.RuleTemplateBo;
34  import org.kuali.rice.kew.service.KEWServiceLocator;
35  import org.kuali.rice.kew.api.KewApiConstants;
36  import org.kuali.rice.kew.util.Utilities;
37  import org.kuali.rice.kim.api.group.Group;
38  import org.kuali.rice.kim.api.identity.principal.Principal;
39  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
40  import org.xml.sax.SAXException;
41  
42  import javax.xml.parsers.ParserConfigurationException;
43  import java.io.IOException;
44  import java.io.InputStream;
45  import java.sql.Timestamp;
46  import java.text.ParseException;
47  import java.util.ArrayList;
48  import java.util.Iterator;
49  import java.util.List;
50  
51  import static org.kuali.rice.core.api.impex.xml.XmlConstants.*;
52  
53  /**
54   * Parses rules from XML.
55   *
56   * @see RuleBaseValues
57   *
58   * @author Kuali Rice Team (rice.collab@kuali.org)
59   */
60  public class RuleXmlParser {
61  
62      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(RuleXmlParser.class);
63  
64      /**
65       * Priority to use if rule responsibility omits priority
66       */
67      private static final int DEFAULT_RULE_PRIORITY = 1;
68      /**
69       * Value of Force Action flag if omitted; default to false, we will NOT force action for approvals
70       */
71      private static final boolean DEFAULT_FORCE_ACTION = false;
72      /**
73       * Default approve policy, if omitted; defaults to FIRST_APPROVE, the request will be satisfied by the first approval
74       */
75      private static final String DEFAULT_APPROVE_POLICY = ActionRequestPolicy.FIRST.getCode();
76      /**
77       * Default action requested, if omitted; defaults to "A"pprove
78       */
79      private static final String DEFAULT_ACTION_REQUESTED = KewApiConstants.ACTION_REQUEST_APPROVE_REQ;
80  
81      public List<RuleDelegationBo> parseRuleDelegations(InputStream input) throws IOException, XmlException {
82      	try {
83              Document doc = XmlHelper.trimSAXXml(input);
84              Element root = doc.getRootElement();
85              return parseRuleDelegations(root);
86          } catch (JDOMException e) {
87              throw new XmlException("Parse error.", e);
88          } catch (SAXException e){
89              throw new XmlException("Parse error.",e);
90          } catch(ParserConfigurationException e){
91              throw new XmlException("Parse error.",e);
92          }
93      }
94      
95      public List<RuleBaseValues> parseRules(InputStream input) throws IOException, XmlException {
96          try {
97              Document doc = XmlHelper.trimSAXXml(input);
98              Element root = doc.getRootElement();
99              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 }