001 /**
002 * Copyright 2005-2014 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 */
016 package org.kuali.rice.kew.xml.xstream;
017
018 import java.util.ArrayList;
019 import java.util.Iterator;
020 import java.util.List;
021
022 import javax.xml.xpath.XPath;
023 import javax.xml.xpath.XPathConstants;
024 import javax.xml.xpath.XPathExpressionException;
025
026 import org.apache.commons.lang.StringUtils;
027 import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
028 import org.w3c.dom.NamedNodeMap;
029 import org.w3c.dom.Node;
030 import org.w3c.dom.NodeList;
031
032
033 /**
034 * Evaluates simple XPath expressions to follow paths through a document generated by XStream which uses
035 * "reference" elements to handle circular and duplicate references. For example, an XML document
036 * generated from XStream might look like the following:
037 *
038 * <pre><test>
039 * <a>hello</a>
040 * <b>
041 * <a reference="../../a"/>
042 * </b>
043 * </test></pre>
044 *
045 * <p>In the above case, the XPath expression /test/a would result in the "hello" text but the
046 * XPath expression /test/b/a would result in the empty string. However, if the evaluator below is mapped
047 * as an XPath function, than it could be used as follows on the second expression to produce the desired result of "hello":
048 * xstreamsafe('/test/b/a', root())
049 *
050 * @author Kuali Rice Team (rice.collab@kuali.org)
051 */
052 public class XStreamSafeEvaluator {
053
054 private static final String MATCH_ANY = "//";
055 private static final String MATCH_ROOT = "/";
056 private static final String MATCH_CURRENT = ".";
057 private static final String XSTREAM_REFERENCE_ATTRIBUTE = "reference";
058 private XPath xpath;
059
060 public XStreamSafeEvaluator() {}
061
062 public XStreamSafeEvaluator(XPath xpath) {
063 this.xpath = xpath;
064 }
065 /**
066 * Evaluates the given XPath expression against the given Node while following reference attributes
067 * on Nodes in a way which is compatible with the XStream library.
068 *
069 * @throws XPathExpressionException if there was a problem evaluation the XPath expression.
070 */
071 public NodeList evaluate(String xPathExpression, Node rootSearchNode) throws XPathExpressionException {
072 XPath xpathEval = this.getXpath();
073 List segments = new ArrayList();
074 parseExpression(segments, xPathExpression, true);
075 SimpleNodeList nodes = new SimpleNodeList();
076 nodes.getList().add(rootSearchNode);
077 for (Iterator iterator = segments.iterator(); iterator.hasNext();) {
078 SimpleNodeList newNodeList = new SimpleNodeList();
079 XPathSegment expression = (XPathSegment) iterator.next();
080 for (Iterator nodeIterator = nodes.getList().iterator(); nodeIterator.hasNext();) {
081 Node node = (Node)nodeIterator.next();
082 node = resolveNodeReference(xpathEval, node);
083 if (node != null) {
084 NodeList evalSet = (NodeList)xpathEval.evaluate(expression.getXPathExpression(), node, XPathConstants.NODESET);
085 if (evalSet != null) {
086 for (int nodeIndex = 0; nodeIndex < evalSet.getLength(); nodeIndex++) {
087 Node newNode = evalSet.item(nodeIndex);
088 newNodeList.getList().add(newNode);
089 }
090 }
091 }
092 }
093 nodes = newNodeList;
094 }
095 // now, after we've reached "the end of the line" check our leaf nodes and resolve any XStream references on them
096 // 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
097 // probably a more elegent way to integrate it with the algorithm above...
098 SimpleNodeList newNodes = new SimpleNodeList();
099 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 }