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.role;
17  
18  import org.apache.commons.lang.StringUtils;
19  import org.kuali.rice.core.api.exception.RiceRuntimeException;
20  import org.kuali.rice.core.api.util.xml.XmlJotter;
21  import org.kuali.rice.kew.api.KewApiConstants;
22  import org.kuali.rice.kew.api.extension.ExtensionDefinition;
23  import org.kuali.rice.kew.engine.RouteContext;
24  import org.kuali.rice.kew.rule.XmlConfiguredAttribute;
25  import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
26  import org.w3c.dom.Document;
27  import org.w3c.dom.Element;
28  import org.w3c.dom.Node;
29  import org.w3c.dom.NodeList;
30  import org.xml.sax.InputSource;
31  
32  import javax.xml.xpath.XPath;
33  import javax.xml.xpath.XPathConstants;
34  import javax.xml.xpath.XPathExpressionException;
35  import java.io.StringReader;
36  import java.util.ArrayList;
37  import java.util.HashMap;
38  import java.util.List;
39  import java.util.Map;
40  
41  /**
42   * Resolves qualifiers based on XPath configuration in the resolver's attribute.
43   * 
44   * <p>An example of the xml processed by this attribute follows:
45   * 
46   * <p><pre>
47   * <resolverConfig>
48   *   <baseXPathExpression>/xmlData/chartOrg</baseXPathExpression>
49   *   <attributes name="chart">
50   *     <xPathExpression>./chart</xPathExpression>
51   *   </attributes>
52   *   <attributes name="org">
53   *     <xPathExpression>./org</xPathExpression>
54   *   </attributes>
55   * </resolverConfig>
56   * </pre>
57   * 
58   * <p>There are 2 different types of qualifier resolvers, those that resolve compound
59   * attribute sets and those that resolve simple attribute sets.  A simple attribute
60   * set is one which includes only a single "qualifier" specification.  The example above
61   * is compound because it includes both chart and org.
62   * 
63   * <p>When dealing with compound attribute sets, the baseXPathExpression is used to
64   * define grouping for these compound sets.  It is therefore required that inside each
65   * resulting element retrieved from the baseXPathExpression, there is only a single instance
66   * of each qualifier.  If this is not the case, an error will be thrown.  For the example
67   * above, the following XML would be evaluated successfully:
68   * 
69   * <p><pre>
70   * <xmlData>
71   *   <chartOrg>
72   *     <chart>BL</chart>
73   *     <org>BUS</org>
74   *   </chartOrg>
75   *   <chartOrg>
76   *     <chart>IN</chart>
77   *     <org>MED</org>
78   *   </chartOrg>
79   * </xmlData>
80   * </pre>
81   * 
82   * <p>This would return 2 attributes sets, each with a chart and org in it.  The following
83   * XML would cause the XPathQualifierResolver to throw an exception during processing.
84   * 
85   * <p><pre>
86   * <xmlData>
87   *   <chartOrg>
88   *     <chart>BL</chart>
89   *     <org>BUS</org>
90   *     <chart>IN</chart>
91   *     <org>MED</org>
92   *   </chartOrg>
93   * </xmlData>
94   * </pre>
95   * 
96   * <p>In this case the resolver has no knowledge of how to group chart and org together.
97   * What follows is an example of a resolver using a simple attribute set:
98   * 
99   * <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  */
125 public 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 }