View Javadoc

1   /**
2    * Copyright 2005-2014 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.xstream;
17  
18  import java.util.ArrayList;
19  import java.util.Iterator;
20  import java.util.List;
21  
22  import javax.xml.xpath.XPath;
23  import javax.xml.xpath.XPathConstants;
24  import javax.xml.xpath.XPathExpressionException;
25  
26  import org.apache.commons.lang.StringUtils;
27  import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
28  import org.w3c.dom.NamedNodeMap;
29  import org.w3c.dom.Node;
30  import org.w3c.dom.NodeList;
31  
32  
33  /**
34   * Evaluates simple XPath expressions to follow paths through a document generated by XStream which uses
35   * "reference" elements to handle circular and duplicate references.  For example, an XML document 
36   * generated from XStream might look like the following:
37   * 
38   * <pre><test>
39   *   <a>hello</a>
40   *   <b>
41   *     <a reference="../../a"/>
42   *   </b>
43   * </test></pre>
44   * 
45   * <p>In the above case, the XPath expression /test/a would result in the "hello" text but the 
46   * XPath expression /test/b/a would result in the empty string.  However, if the evaluator below is mapped
47   * as an XPath function, than it could be used as follows on the second expression to produce the desired result of "hello": 
48   * xstreamsafe('/test/b/a', root())
49   * 
50   * @author Kuali Rice Team (rice.collab@kuali.org)
51   */
52  public class XStreamSafeEvaluator {
53  	
54  	private static final String MATCH_ANY = "//";
55  	private static final String MATCH_ROOT = "/";
56  	private static final String MATCH_CURRENT = ".";
57  	private static final String XSTREAM_REFERENCE_ATTRIBUTE = "reference";
58  	private XPath xpath;
59  
60  	public XStreamSafeEvaluator() {}
61  	
62  	public XStreamSafeEvaluator(XPath xpath) {
63  		this.xpath = xpath;
64  	}
65  	/**
66  	 * Evaluates the given XPath expression against the given Node while following reference attributes
67  	 * on Nodes in a way which is compatible with the XStream library.
68  	 *
69  	 * @throws XPathExpressionException if there was a problem evaluation the XPath expression.
70  	 */
71  	public NodeList evaluate(String xPathExpression, Node rootSearchNode) throws XPathExpressionException {
72  		XPath xpathEval = this.getXpath();
73  		List segments = new ArrayList();
74  		parseExpression(segments, xPathExpression, true);
75  		SimpleNodeList nodes = new SimpleNodeList();
76  		nodes.getList().add(rootSearchNode);
77  		for (Iterator iterator = segments.iterator(); iterator.hasNext();) {
78  			SimpleNodeList newNodeList = new SimpleNodeList();
79  			XPathSegment expression = (XPathSegment) iterator.next();
80  			for (Iterator nodeIterator = nodes.getList().iterator(); nodeIterator.hasNext();) {
81  				Node node = (Node)nodeIterator.next();
82  				node = resolveNodeReference(xpathEval, node);
83  				if (node != null) {
84  					NodeList evalSet = (NodeList)xpathEval.evaluate(expression.getXPathExpression(), node, XPathConstants.NODESET);
85  					if (evalSet != null) {
86  						for (int nodeIndex = 0; nodeIndex < evalSet.getLength(); nodeIndex++) {
87  							Node newNode = evalSet.item(nodeIndex);
88  							newNodeList.getList().add(newNode);
89  						}
90  					}
91  				}
92  			}
93  			nodes = newNodeList;
94  		}
95  		// now, after we've reached "the end of the line" check our leaf nodes and resolve any XStream references on them
96  		// TODO I noticed that the original implementation of this method was not doing the following work so I'm just tacking it on the end, there's
97  		// probably a more elegent way to integrate it with the algorithm above...
98  		SimpleNodeList newNodes = new SimpleNodeList();
99  		for (Iterator iterator = nodes.getList().iterator(); iterator.hasNext();) {
100 			Node node = (Node) iterator.next();
101 			newNodes.getList().add(resolveNodeReference(xpathEval, node));
102 		}
103 		return newNodes;
104 	}
105 	
106 	/**
107 	 * Parses the given XPath expression into a List of segments which can be evaluated in order.
108 	 */
109 	private void parseExpression(List segments, String xPathExpression, boolean isInitialSegment) throws XPathExpressionException {
110 		if (StringUtils.isEmpty(xPathExpression)) {
111 			return;
112 		}
113 		XPathSegment segment = isInitialSegment ? parseInitialSegment(xPathExpression) : parseNextSegment(xPathExpression);
114 		segments.add(segment);
115 		parseExpression(segments, xPathExpression.substring(segment.getLength()), false);
116 	}
117 
118 	
119 //	private XPathSegment parseNextSegment(String xPathExpression) throws XPathExpressionException {
120 //		int operatorLength = 2;
121 //		int firstIndex = xPathExpression.indexOf(MATCH_ANY);
122 //		if (firstIndex != 0) {
123 //			firstIndex = xPathExpression.indexOf(MATCH_CURRENT);
124 //			if (firstIndex != 0) {
125 //				operatorLength = 1;
126 //				firstIndex = xPathExpression.indexOf(MATCH_ROOT);				
127 //			}
128 //		}
129 //		// the operator should be at the beginning of the string
130 //		if (firstIndex != 0) {
131 //			throw new XPathExpressionException("Could not locate an appropriate ./, /, or // operator at the begginingg of the xpath segment: " + xPathExpression);
132 //		}
133 //		int nextIndex = xPathExpression.indexOf(MATCH_ANY, operatorLength);
134 //		if (nextIndex == -1) {
135 //			nextIndex = xPathExpression.indexOf(MATCH_ROOT, operatorLength);
136 //		}
137 //		if (nextIndex == -1) {
138 //			nextIndex = xPathExpression.length();
139 //		}
140 //		return new XPathSegment(xPathExpression.substring(0,operatorLength),
141 //				xPathExpression.substring(operatorLength, nextIndex));
142 //	}
143 	
144 	/**
145 	 * Parses the next segment of the given XPath expression by grabbing the first
146 	 * segment off of the given xpath expression.  The given xpath expression must
147 	 * start with either ./, /, or // otherwise an XPathExpressionException is thrown.
148 	 */
149 	private XPathSegment parseInitialSegment(String xPathExpression) throws XPathExpressionException {
150 		// TODO we currently can't support expressions that start with .//
151 		if (xPathExpression.startsWith(MATCH_CURRENT+MATCH_ANY)) {
152 			throw new XPathExpressionException("XStream safe evaluator currenlty does not support expressions that start with " +MATCH_CURRENT+MATCH_ANY);
153 		}
154 		//int operatorLength = 3;
155 		//int firstIndex = xPathExpression.indexOf(MATCH_CURRENT+MATCH_ANY);
156 		//if (firstIndex != 0) {
157 			int operatorLength = 2;
158 			int firstIndex = xPathExpression.indexOf(MATCH_CURRENT+MATCH_ROOT);
159 			if (firstIndex != 0) {
160 				firstIndex = xPathExpression.indexOf(MATCH_ANY);
161 				if (firstIndex != 0) {
162 					operatorLength = 1;
163 					firstIndex = xPathExpression.indexOf(MATCH_ROOT);
164 				}
165 			}
166 		//}
167 		// the operator should be at the beginning of the string
168 		if (firstIndex != 0) {
169 			throw new XPathExpressionException("Could not locate an appropriate ./, /, or // operator at the begginingg of the xpath segment: " + xPathExpression);
170 		}
171 		int nextIndex = xPathExpression.indexOf(MATCH_ROOT, operatorLength);
172 		if (nextIndex == -1) {
173 			nextIndex = xPathExpression.length();
174 		}
175 		return new XPathSegment(xPathExpression.substring(0, operatorLength),
176 				xPathExpression.substring(operatorLength, nextIndex), true);
177 	}
178 
179 	/**
180 	 * Parses the next segment of the given XPath expression by grabbing the first
181 	 * segment off of the given xpath expression.  The given xpath expression must
182 	 * start with / otherwise an XPathExpressionException is thrown.  This is because
183 	 * the "next" segments represent the internal pieces in an XPath expression.
184 	 */
185 	private XPathSegment parseNextSegment(String xPathExpression) throws XPathExpressionException {
186 		if (!xPathExpression.startsWith(MATCH_ROOT)) {
187 			throw new XPathExpressionException("Illegal xPath segment, the given segment is not a valid segment and should start with a '"+MATCH_ROOT+"'.  Value was: " + xPathExpression);
188 		}
189 		int operatorLength = MATCH_ROOT.length();
190 		int nextIndex = xPathExpression.indexOf(MATCH_ROOT, operatorLength);
191 		if (nextIndex == -1) {
192 			nextIndex = xPathExpression.length();
193 		}
194 		return new XPathSegment(MATCH_CURRENT+MATCH_ROOT, xPathExpression.substring(operatorLength, nextIndex), false);
195 	}
196 	
197 	/**
198 	 * Resolves the reference to a Node by checking for a "reference" attribute and returning the resolved node if
199 	 * it's there.  The resolution happens by grabbing the value of the reference and evaluation it as an XPath
200 	 * expression against the given Node.  If there is no reference attribute, the node passed in is returned.
201 	 * The method is recursive in the fact that it will continue to follow XStream "reference" attributes until it
202 	 * reaches a resolved node.
203 	 */
204 	private Node resolveNodeReference(XPath xpath, Node node) throws XPathExpressionException{
205 		NamedNodeMap attributes = node.getAttributes();
206 		if (attributes != null) {
207 			Node referenceNode = attributes.getNamedItem(XSTREAM_REFERENCE_ATTRIBUTE);
208 			if (referenceNode != null) {
209 				node = (Node)xpath.evaluate(referenceNode.getNodeValue(), node, XPathConstants.NODE);
210 				if (node != null) {
211 					node = resolveNodeReference(xpath, node);
212 				} else {
213 					throw new XPathExpressionException("Could not locate the node for the given XStream references expression: '" + referenceNode.getNodeValue() + "'");
214 				}
215 			}
216 		}
217 		return node;
218 	}
219 
220 	/**
221 	 * A single segment of an XPath expression.
222 	 */
223 	private class XPathSegment {
224 		private final String operator;
225 		private final String value;
226 		private final boolean isInitialSegment;
227 		public XPathSegment(String operator, String value, boolean isInitialSegment) {
228 			this.operator = operator;
229 			this.value = value;
230 			// if it's not an initial segment then a '.' will preceed the operator and should not be counted in the length
231 			this.isInitialSegment = isInitialSegment;
232 		}
233 		public int getLength() {
234 			// if it's not an initial segment then a '.' will preceed the operator and should not be counted in the length
235 			if (!isInitialSegment) {
236 				return operator.length() + value.length() - 1;
237 			}
238 			return operator.length() + value.length();
239 		}
240 		/**
241 		 * Returns an XPath expression which can be evaluated in the context of the 
242 		 * node returned by the previously executed segment.
243 		 */
244 		public String getXPathExpression() {
245 			return operator+value;
246 		}
247 	}
248 	
249 	/**
250 	 * A simple NodeList implementation, as simple as it gets.  This allows us to not be tied to
251 	 * any particular XML service provider's NodeList implementation.
252 	 */
253 	private class SimpleNodeList implements NodeList {
254 		private List nodes = new ArrayList();
255 		public Node item(int index) {
256 			return (Node)nodes.get(index);
257 		}
258 		public int getLength() {
259 			return nodes.size();
260 		}
261 		public List getList() {
262 			return nodes;
263 		}
264 	}
265 	
266 	public XPath getXpath() {
267 		if (this.xpath == null) {
268 			return XPathHelper.newXPath();
269 		}
270 		return xpath;
271 	}
272 
273 	public void setXpath(XPath xpath) {
274 		this.xpath = xpath;
275 	}
276 
277 }