001/** 002 * Copyright 2005-2017 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.role; 017 018import org.apache.commons.lang.StringUtils; 019import org.kuali.rice.core.api.exception.RiceRuntimeException; 020import org.kuali.rice.core.api.util.xml.XmlJotter; 021import org.kuali.rice.kew.api.KewApiConstants; 022import org.kuali.rice.kew.api.extension.ExtensionDefinition; 023import org.kuali.rice.kew.engine.RouteContext; 024import org.kuali.rice.kew.rule.XmlConfiguredAttribute; 025import org.kuali.rice.kew.rule.xmlrouting.XPathHelper; 026import org.w3c.dom.Document; 027import org.w3c.dom.Element; 028import org.w3c.dom.Node; 029import org.w3c.dom.NodeList; 030import org.xml.sax.InputSource; 031 032import javax.xml.xpath.XPath; 033import javax.xml.xpath.XPathConstants; 034import javax.xml.xpath.XPathExpressionException; 035import java.io.StringReader; 036import java.util.ArrayList; 037import java.util.HashMap; 038import java.util.List; 039import java.util.Map; 040 041/** 042 * Resolves qualifiers based on XPath configuration in the resolver's attribute. 043 * 044 * <p>An example of the xml processed by this attribute follows: 045 * 046 * <p><pre> 047 * <resolverConfig> 048 * <baseXPathExpression>/xmlData/chartOrg</baseXPathExpression> 049 * <attributes name="chart"> 050 * <xPathExpression>./chart</xPathExpression> 051 * </attributes> 052 * <attributes name="org"> 053 * <xPathExpression>./org</xPathExpression> 054 * </attributes> 055 * </resolverConfig> 056 * </pre> 057 * 058 * <p>There are 2 different types of qualifier resolvers, those that resolve compound 059 * attribute sets and those that resolve simple attribute sets. A simple attribute 060 * set is one which includes only a single "qualifier" specification. The example above 061 * is compound because it includes both chart and org. 062 * 063 * <p>When dealing with compound attribute sets, the baseXPathExpression is used to 064 * define grouping for these compound sets. It is therefore required that inside each 065 * resulting element retrieved from the baseXPathExpression, there is only a single instance 066 * of each qualifier. If this is not the case, an error will be thrown. For the example 067 * above, the following XML would be evaluated successfully: 068 * 069 * <p><pre> 070 * <xmlData> 071 * <chartOrg> 072 * <chart>BL</chart> 073 * <org>BUS</org> 074 * </chartOrg> 075 * <chartOrg> 076 * <chart>IN</chart> 077 * <org>MED</org> 078 * </chartOrg> 079 * </xmlData> 080 * </pre> 081 * 082 * <p>This would return 2 attributes sets, each with a chart and org in it. The following 083 * XML would cause the XPathQualifierResolver to throw an exception during processing. 084 * 085 * <p><pre> 086 * <xmlData> 087 * <chartOrg> 088 * <chart>BL</chart> 089 * <org>BUS</org> 090 * <chart>IN</chart> 091 * <org>MED</org> 092 * </chartOrg> 093 * </xmlData> 094 * </pre> 095 * 096 * <p>In this case the resolver has no knowledge of how to group chart and org together. 097 * What follows is an example of a resolver using a simple attribute set: 098 * 099 * <p><pre> 100 * <resolverConfig> 101 * <baseXPathExpression>/xmlData/accountNumbers</baseXPathExpression> 102 * <attributes name="accountNumber"> 103 * <xPathExpression>./accountNumber</xPathExpression> 104 * </attributes> 105 * </resolverConfig> 106 * </pre> 107 * 108 * <p>In this example, the following XML would return a List containing an Map<String, String> 109 * for each account number when resolved. 110 * 111 * <p><pre> 112 * <xmlData> 113 * <accountNumbers> 114 * <accountNumber>12345</accountNumber> 115 * <accountNumber>54321</accountNumber> 116 * <accountNumber>102030</accountNumber> 117 * <accountNumber>302010</accountNumber> 118 * </accountNumbers> 119 * </xmlData> 120 * 121 * <p>The baseXPathExpression is optional and defaults to the root of the document if not specified. 122 * 123 * @author Kuali Rice Team (rice.collab@kuali.org) 124 */ 125public class XPathQualifierResolver implements QualifierResolver, XmlConfiguredAttribute { 126 private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(XPathQualifierResolver.class); 127 128 private ExtensionDefinition extensionDefinition; 129 130 public List<Map<String, String>> resolve(RouteContext context) { 131 ResolverConfig config = parseResolverConfig(); 132 Document xmlContent = context.getDocumentContent().getDocument(); 133 XPath xPath = XPathHelper.newXPath(); 134 boolean isCompoundMap = config.getExpressionMap().size() > 1; 135 try { 136 List<Map<String, String>> maps = new ArrayList<Map<String, String>>(); 137 NodeList baseElements = (NodeList)xPath.evaluate(config.getBaseXPathExpression(), xmlContent, XPathConstants.NODESET); 138 if (LOG.isDebugEnabled()) { 139 LOG.debug("Found " + baseElements.getLength() + " baseElements to parse for Map<String, String>s using document XML:" + XmlJotter.jotDocument(xmlContent)); 140 } 141 for (int index = 0; index < baseElements.getLength(); index++) { 142 Node baseNode = baseElements.item(index); 143 if (isCompoundMap) { 144 handleCompoundMap(baseNode, maps, config, xPath); 145 } else { 146 handleSimpleMap(baseNode, maps, config, xPath); 147 } 148 } 149 return maps; 150 } catch (XPathExpressionException e) { 151 throw new RiceRuntimeException("Encountered an issue executing XPath.", e); 152 } 153 } 154 155 protected void handleCompoundMap(Node baseNode, List<Map<String, String>> maps, ResolverConfig config, XPath xPath) throws XPathExpressionException { 156 Map<String, String> map = new HashMap<String, String>(); 157 for (String attributeName : config.getExpressionMap().keySet()) { 158 String xPathExpression = config.getExpressionMap().get(attributeName); 159 NodeList attributes = (NodeList)xPath.evaluate(xPathExpression, baseNode, XPathConstants.NODESET); 160 if (attributes.getLength() > 1) { 161 throw new RiceRuntimeException("Found more than more XPath result for an attribute in a compound attribute set for attribute: " + attributeName + " with expression " + xPathExpression); 162 } else if (attributes.getLength() != 0) { 163 String attributeValue = ((Element)attributes.item(0)).getTextContent(); 164 if (LOG.isDebugEnabled()) { 165 LOG.debug("Adding values to compound Map<String, String>: " + attributeName + "::" + attributeValue); 166 } 167 map.put(attributeName, attributeValue); 168 } 169 } 170 maps.add(map); 171 } 172 173 protected void handleSimpleMap(Node baseNode, List<Map<String, String>> maps, ResolverConfig config, XPath xPath) throws XPathExpressionException { 174 String attributeName = config.getExpressionMap().keySet().iterator().next(); 175 String xPathExpression = config.getExpressionMap().get(attributeName); 176 NodeList attributes = (NodeList)xPath.evaluate(xPathExpression, baseNode, XPathConstants.NODESET); 177 for (int index = 0; index < attributes.getLength(); index++) { 178 Element attributeElement = (Element)attributes.item(index); 179 Map<String, String> map = new HashMap<String, String>(); 180 String attributeValue = attributeElement.getTextContent(); 181 if (LOG.isDebugEnabled()) { 182 LOG.debug("Adding values to simple Map<String, String>: " + attributeName + "::" + attributeValue); 183 } 184 map.put(attributeName, attributeValue); 185 maps.add(map); 186 } 187 } 188 189 protected ResolverConfig parseResolverConfig() { 190 if (extensionDefinition.getConfiguration() != null 191 && extensionDefinition.getConfiguration().get(KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA) == null) { 192 throw new RiceRuntimeException("Failed to locate a RuleAttribute for the given XPathQualifierResolver"); 193 } 194 try { 195 ResolverConfig resolverConfig = new ResolverConfig(); 196 String xmlConfig = extensionDefinition.getConfiguration().get(KewApiConstants.ATTRIBUTE_XML_CONFIG_DATA); 197 XPath xPath = XPathHelper.newXPath(); 198 String baseExpression = xPath.evaluate("//resolverConfig/baseXPathExpression", new InputSource(new StringReader(xmlConfig))); 199 if (!StringUtils.isEmpty(baseExpression)) { 200 resolverConfig.setBaseXPathExpression(baseExpression); 201 } 202 //We need to check for two possible xml configurations 203 //1 - 'attributes' 204 //2- 'qualifier' (legacy) 205 NodeList qualifiers = (NodeList)xPath.evaluate("//resolverConfig/attributes", new InputSource(new StringReader(xmlConfig)), XPathConstants.NODESET); 206 NodeList qualifiersLegacy = (NodeList)xPath.evaluate("//resolverConfig/qualifier", new InputSource(new StringReader(xmlConfig)), XPathConstants.NODESET); 207 208 if ((qualifiers == null || qualifiers.getLength() == 0) && (qualifiersLegacy == null || qualifiersLegacy.getLength() == 0)) { 209 throw new RiceRuntimeException("Invalid qualifier resolver configuration. Must contain at least one qualifier!"); 210 } 211 //check for standard qualifiers (those using 'attributes' xml elements) and add if they exist 212 for (int index = 0; index < qualifiers.getLength(); index++) { 213 Element qualifierElement = (Element)qualifiers.item(index); 214 String name = qualifierElement.getAttribute("name"); 215 NodeList expressions = qualifierElement.getElementsByTagName("xPathExpression"); 216 if (expressions.getLength() != 1) { 217 throw new RiceRuntimeException("There should only be a single xPathExpression per qualifier"); 218 } 219 Element expressionElement = (Element)expressions.item(0); 220 resolverConfig.getExpressionMap().put(name, expressionElement.getTextContent()); 221 } 222 223 //check for legacy qualifiers (those using 'qualifier' xml elements) and add if they exist 224 for (int index = 0; index < qualifiersLegacy.getLength(); index++) { 225 Element qualifierElement = (Element)qualifiersLegacy.item(index); 226 String name = qualifierElement.getAttribute("name"); 227 NodeList expressions = qualifierElement.getElementsByTagName("xPathExpression"); 228 if (expressions.getLength() != 1) { 229 throw new RiceRuntimeException("There should only be a single xPathExpression per qualifier"); 230 } 231 Element expressionElement = (Element)expressions.item(0); 232 resolverConfig.getExpressionMap().put(name, expressionElement.getTextContent()); 233 } 234 if (LOG.isDebugEnabled()) { 235 LOG.debug("Using Resolver Config Settings: " + resolverConfig.toString()); 236 } 237 return resolverConfig; 238 } catch (XPathExpressionException e) { 239 throw new RiceRuntimeException("Encountered an error parsing resolver config.", e); 240 } 241 } 242 243 @Override 244 public void setExtensionDefinition(ExtensionDefinition ruleAttribute) { 245 extensionDefinition = ruleAttribute; 246 } 247 248 class ResolverConfig { 249 private String baseXPathExpression = "/"; 250 private Map<String, String> expressionMap = new HashMap<String, String>(); 251 public String getBaseXPathExpression() { 252 return this.baseXPathExpression; 253 } 254 public void setBaseXPathExpression(String baseXPathExpression) { 255 this.baseXPathExpression = baseXPathExpression; 256 } 257 public Map<String, String> getExpressionMap() { 258 return this.expressionMap; 259 } 260 public void setExpressionMap(Map<String, String> expressionMap) { 261 this.expressionMap = expressionMap; 262 } 263 @Override 264 public String toString() { 265 StringBuffer sb = new StringBuffer(); 266 sb.append( '\n' ); 267 sb.append("ResolverConfig Parameters\n"); 268 sb.append( " baseXPathExpression: " + baseXPathExpression + "\n" ); 269 sb.append( " expressionMap:\n" ); 270 for (Map.Entry<String, String> entry : expressionMap.entrySet()) { 271 sb.append( " " + entry.getKey() + ": " + entry.getValue() + "\n" ); 272 } 273 return sb.toString(); 274 } 275 } 276 277}