Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
XStreamSafeEvaluator |
|
| 2.8;2.8 | ||||
XStreamSafeEvaluator$1 |
|
| 2.8;2.8 | ||||
XStreamSafeEvaluator$SimpleNodeList |
|
| 2.8;2.8 | ||||
XStreamSafeEvaluator$XPathSegment |
|
| 2.8;2.8 |
1 | /** | |
2 | * Copyright 2005-2011 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 | 0 | public XStreamSafeEvaluator() {} |
61 | ||
62 | 0 | public XStreamSafeEvaluator(XPath xpath) { |
63 | 0 | this.xpath = xpath; |
64 | 0 | } |
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 | 0 | XPath xpathEval = this.getXpath(); |
73 | 0 | List segments = new ArrayList(); |
74 | 0 | parseExpression(segments, xPathExpression, true); |
75 | 0 | SimpleNodeList nodes = new SimpleNodeList(); |
76 | 0 | nodes.getList().add(rootSearchNode); |
77 | 0 | for (Iterator iterator = segments.iterator(); iterator.hasNext();) { |
78 | 0 | SimpleNodeList newNodeList = new SimpleNodeList(); |
79 | 0 | XPathSegment expression = (XPathSegment) iterator.next(); |
80 | 0 | for (Iterator nodeIterator = nodes.getList().iterator(); nodeIterator.hasNext();) { |
81 | 0 | Node node = (Node)nodeIterator.next(); |
82 | 0 | node = resolveNodeReference(xpathEval, node); |
83 | 0 | if (node != null) { |
84 | 0 | NodeList evalSet = (NodeList)xpathEval.evaluate(expression.getXPathExpression(), node, XPathConstants.NODESET); |
85 | 0 | if (evalSet != null) { |
86 | 0 | for (int nodeIndex = 0; nodeIndex < evalSet.getLength(); nodeIndex++) { |
87 | 0 | Node newNode = evalSet.item(nodeIndex); |
88 | 0 | newNodeList.getList().add(newNode); |
89 | } | |
90 | } | |
91 | } | |
92 | 0 | } |
93 | 0 | nodes = newNodeList; |
94 | 0 | } |
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 | 0 | SimpleNodeList newNodes = new SimpleNodeList(); |
99 | 0 | for (Iterator iterator = nodes.getList().iterator(); iterator.hasNext();) { |
100 | 0 | Node node = (Node) iterator.next(); |
101 | 0 | newNodes.getList().add(resolveNodeReference(xpathEval, node)); |
102 | 0 | } |
103 | 0 | 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 | 0 | if (StringUtils.isEmpty(xPathExpression)) { |
111 | 0 | return; |
112 | } | |
113 | 0 | XPathSegment segment = isInitialSegment ? parseInitialSegment(xPathExpression) : parseNextSegment(xPathExpression); |
114 | 0 | segments.add(segment); |
115 | 0 | parseExpression(segments, xPathExpression.substring(segment.getLength()), false); |
116 | 0 | } |
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 | 0 | if (xPathExpression.startsWith(MATCH_CURRENT+MATCH_ANY)) { |
152 | 0 | 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 | 0 | int operatorLength = 2; |
158 | 0 | int firstIndex = xPathExpression.indexOf(MATCH_CURRENT+MATCH_ROOT); |
159 | 0 | if (firstIndex != 0) { |
160 | 0 | firstIndex = xPathExpression.indexOf(MATCH_ANY); |
161 | 0 | if (firstIndex != 0) { |
162 | 0 | operatorLength = 1; |
163 | 0 | firstIndex = xPathExpression.indexOf(MATCH_ROOT); |
164 | } | |
165 | } | |
166 | //} | |
167 | // the operator should be at the beginning of the string | |
168 | 0 | if (firstIndex != 0) { |
169 | 0 | throw new XPathExpressionException("Could not locate an appropriate ./, /, or // operator at the begginingg of the xpath segment: " + xPathExpression); |
170 | } | |
171 | 0 | int nextIndex = xPathExpression.indexOf(MATCH_ROOT, operatorLength); |
172 | 0 | if (nextIndex == -1) { |
173 | 0 | nextIndex = xPathExpression.length(); |
174 | } | |
175 | 0 | 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 | 0 | if (!xPathExpression.startsWith(MATCH_ROOT)) { |
187 | 0 | 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 | 0 | int operatorLength = MATCH_ROOT.length(); |
190 | 0 | int nextIndex = xPathExpression.indexOf(MATCH_ROOT, operatorLength); |
191 | 0 | if (nextIndex == -1) { |
192 | 0 | nextIndex = xPathExpression.length(); |
193 | } | |
194 | 0 | 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 | 0 | NamedNodeMap attributes = node.getAttributes(); |
206 | 0 | if (attributes != null) { |
207 | 0 | Node referenceNode = attributes.getNamedItem(XSTREAM_REFERENCE_ATTRIBUTE); |
208 | 0 | if (referenceNode != null) { |
209 | 0 | node = (Node)xpath.evaluate(referenceNode.getNodeValue(), node, XPathConstants.NODE); |
210 | 0 | if (node != null) { |
211 | 0 | node = resolveNodeReference(xpath, node); |
212 | } else { | |
213 | 0 | throw new XPathExpressionException("Could not locate the node for the given XStream references expression: '" + referenceNode.getNodeValue() + "'"); |
214 | } | |
215 | } | |
216 | } | |
217 | 0 | 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 | 0 | public XPathSegment(String operator, String value, boolean isInitialSegment) { |
228 | 0 | this.operator = operator; |
229 | 0 | 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 | 0 | this.isInitialSegment = isInitialSegment; |
232 | 0 | } |
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 | 0 | if (!isInitialSegment) { |
236 | 0 | return operator.length() + value.length() - 1; |
237 | } | |
238 | 0 | 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 | 0 | 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 | 0 | private class SimpleNodeList implements NodeList { |
254 | 0 | private List nodes = new ArrayList(); |
255 | public Node item(int index) { | |
256 | 0 | return (Node)nodes.get(index); |
257 | } | |
258 | public int getLength() { | |
259 | 0 | return nodes.size(); |
260 | } | |
261 | public List getList() { | |
262 | 0 | return nodes; |
263 | } | |
264 | } | |
265 | ||
266 | public XPath getXpath() { | |
267 | 0 | if (this.xpath == null) { |
268 | 0 | return XPathHelper.newXPath(); |
269 | } | |
270 | 0 | return xpath; |
271 | } | |
272 | ||
273 | public void setXpath(XPath xpath) { | |
274 | 0 | this.xpath = xpath; |
275 | 0 | } |
276 | ||
277 | } |