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