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}