001: /*
002: * Copyright 2005-2006 The Kuali Foundation.
003: *
004: *
005: * Licensed under the Educational Community License, Version 1.0 (the "License");
006: * you may not use this file except in compliance with the License.
007: * You may obtain a copy of the License at
008: *
009: * http://www.opensource.org/licenses/ecl1.php
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: */
017: package edu.iu.uis.eden.support.xstream;
018:
019: import java.util.ArrayList;
020: import java.util.Iterator;
021: import java.util.List;
022:
023: import javax.xml.xpath.XPath;
024: import javax.xml.xpath.XPathConstants;
025: import javax.xml.xpath.XPathExpressionException;
026:
027: import org.apache.commons.lang.StringUtils;
028: import org.w3c.dom.NamedNodeMap;
029: import org.w3c.dom.Node;
030: import org.w3c.dom.NodeList;
031:
032: import edu.iu.uis.eden.routetemplate.xmlrouting.XPathHelper;
033:
034: /**
035: * Evaluates simple XPath expressions to follow paths through a document generated by XStream which uses
036: * "reference" elements to handle circular and duplicate references. For example, an XML document
037: * generated from XStream might look like the following:
038: *
039: * <pre><test>
040: * <a>hello</a>
041: * <b>
042: * <a reference="../../a"/>
043: * </b>
044: * </test></pre>
045: *
046: * <p>In the above case, the XPath expression /test/a would result in the "hello" text but the
047: * XPath expression /test/b/a would result in the empty string. However, if the evaluator below is mapped
048: * as an XPath function, than it could be used as follows on the second expression to produce the desired result of "hello":
049: * xstreamsafe('/test/b/a', root())
050: *
051: * @author ewestfal
052: */
053: public class XStreamSafeEvaluator {
054:
055: private static final String MATCH_ANY = "//";
056: private static final String MATCH_ROOT = "/";
057: private static final String MATCH_CURRENT = ".";
058: private static final String XSTREAM_REFERENCE_ATTRIBUTE = "reference";
059: private XPath xpath;
060:
061: public XStreamSafeEvaluator() {
062: }
063:
064: public XStreamSafeEvaluator(XPath xpath) {
065: this .xpath = xpath;
066: }
067:
068: /**
069: * Evaluates the given XPath expression against the given Node while following reference attributes
070: * on Nodes in a way which is compatible with the XStream library.
071: *
072: * @throws XPathExpressionException if there was a problem evaluation the XPath expression.
073: */
074: public NodeList evaluate(String xPathExpression, Node rootSearchNode)
075: throws XPathExpressionException {
076: XPath xpathEval = this .getXpath();
077: List segments = new ArrayList();
078: parseExpression(segments, xPathExpression, true);
079: SimpleNodeList nodes = new SimpleNodeList();
080: nodes.getList().add(rootSearchNode);
081: for (Iterator iterator = segments.iterator(); iterator
082: .hasNext();) {
083: SimpleNodeList newNodeList = new SimpleNodeList();
084: XPathSegment expression = (XPathSegment) iterator.next();
085: for (Iterator nodeIterator = nodes.getList().iterator(); nodeIterator
086: .hasNext();) {
087: Node node = (Node) nodeIterator.next();
088: node = resolveNodeReference(xpathEval, node);
089: if (node != null) {
090: NodeList evalSet = (NodeList) xpathEval.evaluate(
091: expression.getXPathExpression(), node,
092: XPathConstants.NODESET);
093: if (evalSet != null) {
094: for (int nodeIndex = 0; nodeIndex < evalSet
095: .getLength(); nodeIndex++) {
096: Node newNode = evalSet.item(nodeIndex);
097: newNodeList.getList().add(newNode);
098: }
099: }
100: }
101: }
102: nodes = newNodeList;
103: }
104: // now, after we've reached "the end of the line" check our leaf nodes and resolve any XStream references on them
105: // 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
106: // probably a more elegent way to integrate it with the algorithm above...
107: SimpleNodeList newNodes = new SimpleNodeList();
108: for (Iterator iterator = nodes.getList().iterator(); iterator
109: .hasNext();) {
110: Node node = (Node) iterator.next();
111: newNodes.getList().add(
112: resolveNodeReference(xpathEval, node));
113: }
114: return newNodes;
115: }
116:
117: /**
118: * Parses the given XPath expression into a List of segments which can be evaluated in order.
119: */
120: private void parseExpression(List segments, String xPathExpression,
121: boolean isInitialSegment) throws XPathExpressionException {
122: if (StringUtils.isEmpty(xPathExpression)) {
123: return;
124: }
125: XPathSegment segment = isInitialSegment ? parseInitialSegment(xPathExpression)
126: : parseNextSegment(xPathExpression);
127: segments.add(segment);
128: parseExpression(segments, xPathExpression.substring(segment
129: .getLength()), false);
130: }
131:
132: // private XPathSegment parseNextSegment(String xPathExpression) throws XPathExpressionException {
133: // int operatorLength = 2;
134: // int firstIndex = xPathExpression.indexOf(MATCH_ANY);
135: // if (firstIndex != 0) {
136: // firstIndex = xPathExpression.indexOf(MATCH_CURRENT);
137: // if (firstIndex != 0) {
138: // operatorLength = 1;
139: // firstIndex = xPathExpression.indexOf(MATCH_ROOT);
140: // }
141: // }
142: // // the operator should be at the beginning of the string
143: // if (firstIndex != 0) {
144: // throw new XPathExpressionException("Could not locate an appropriate ./, /, or // operator at the begginingg of the xpath segment: " + xPathExpression);
145: // }
146: // int nextIndex = xPathExpression.indexOf(MATCH_ANY, operatorLength);
147: // if (nextIndex == -1) {
148: // nextIndex = xPathExpression.indexOf(MATCH_ROOT, operatorLength);
149: // }
150: // if (nextIndex == -1) {
151: // nextIndex = xPathExpression.length();
152: // }
153: // return new XPathSegment(xPathExpression.substring(0,operatorLength),
154: // xPathExpression.substring(operatorLength, nextIndex));
155: // }
156:
157: /**
158: * Parses the next segment of the given XPath expression by grabbing the first
159: * segment off of the given xpath expression. The given xpath expression must
160: * start with either ./, /, or // otherwise an XPathExpressionException is thrown.
161: */
162: private XPathSegment parseInitialSegment(String xPathExpression)
163: throws XPathExpressionException {
164: // TODO we currently can't support expressions that start with .//
165: if (xPathExpression.startsWith(MATCH_CURRENT + MATCH_ANY)) {
166: throw new XPathExpressionException(
167: "XStream safe evaluator currenlty does not support expressions that start with "
168: + MATCH_CURRENT + MATCH_ANY);
169: }
170: //int operatorLength = 3;
171: //int firstIndex = xPathExpression.indexOf(MATCH_CURRENT+MATCH_ANY);
172: //if (firstIndex != 0) {
173: int operatorLength = 2;
174: int firstIndex = xPathExpression.indexOf(MATCH_CURRENT
175: + MATCH_ROOT);
176: if (firstIndex != 0) {
177: firstIndex = xPathExpression.indexOf(MATCH_ANY);
178: if (firstIndex != 0) {
179: operatorLength = 1;
180: firstIndex = xPathExpression.indexOf(MATCH_ROOT);
181: }
182: }
183: //}
184: // the operator should be at the beginning of the string
185: if (firstIndex != 0) {
186: throw new XPathExpressionException(
187: "Could not locate an appropriate ./, /, or // operator at the begginingg of the xpath segment: "
188: + xPathExpression);
189: }
190: int nextIndex = xPathExpression.indexOf(MATCH_ROOT,
191: operatorLength);
192: if (nextIndex == -1) {
193: nextIndex = xPathExpression.length();
194: }
195: return new XPathSegment(xPathExpression.substring(0,
196: operatorLength), xPathExpression.substring(
197: operatorLength, nextIndex), true);
198: }
199:
200: /**
201: * Parses the next segment of the given XPath expression by grabbing the first
202: * segment off of the given xpath expression. The given xpath expression must
203: * start with / otherwise an XPathExpressionException is thrown. This is because
204: * the "next" segments represent the internal pieces in an XPath expression.
205: */
206: private XPathSegment parseNextSegment(String xPathExpression)
207: throws XPathExpressionException {
208: if (!xPathExpression.startsWith(MATCH_ROOT)) {
209: throw new XPathExpressionException(
210: "Illegal xPath segment, the given segment is not a valid segment and should start with a '"
211: + MATCH_ROOT
212: + "'. Value was: "
213: + xPathExpression);
214: }
215: int operatorLength = MATCH_ROOT.length();
216: int nextIndex = xPathExpression.indexOf(MATCH_ROOT,
217: operatorLength);
218: if (nextIndex == -1) {
219: nextIndex = xPathExpression.length();
220: }
221: return new XPathSegment(MATCH_CURRENT + MATCH_ROOT,
222: xPathExpression.substring(operatorLength, nextIndex),
223: false);
224: }
225:
226: /**
227: * Resolves the reference to a Node by checking for a "reference" attribute and returning the resolved node if
228: * it's there. The resolution happens by grabbing the value of the reference and evaluation it as an XPath
229: * expression against the given Node. If there is no reference attribute, the node passed in is returned.
230: * The method is recursive in the fact that it will continue to follow XStream "reference" attributes until it
231: * reaches a resolved node.
232: */
233: private Node resolveNodeReference(XPath xpath, Node node)
234: throws XPathExpressionException {
235: NamedNodeMap attributes = node.getAttributes();
236: if (attributes != null) {
237: Node referenceNode = attributes
238: .getNamedItem(XSTREAM_REFERENCE_ATTRIBUTE);
239: if (referenceNode != null) {
240: node = (Node) xpath.evaluate(referenceNode
241: .getNodeValue(), node, XPathConstants.NODE);
242: if (node != null) {
243: node = resolveNodeReference(xpath, node);
244: } else {
245: throw new XPathExpressionException(
246: "Could not locate the node for the given XStream references expression: '"
247: + referenceNode.getNodeValue()
248: + "'");
249: }
250: }
251: }
252: return node;
253: }
254:
255: /**
256: * A single segment of an XPath expression.
257: */
258: private class XPathSegment {
259: private final String operator;
260: private final String value;
261: private final boolean isInitialSegment;
262:
263: public XPathSegment(String operator, String value,
264: boolean isInitialSegment) {
265: this .operator = operator;
266: this .value = value;
267: // if it's not an initial segment then a '.' will preceed the operator and should not be counted in the length
268: this .isInitialSegment = isInitialSegment;
269: }
270:
271: public int getLength() {
272: // if it's not an initial segment then a '.' will preceed the operator and should not be counted in the length
273: if (!isInitialSegment) {
274: return operator.length() + value.length() - 1;
275: }
276: return operator.length() + value.length();
277: }
278:
279: /**
280: * Returns an XPath expression which can be evaluated in the context of the
281: * node returned by the previously executed segment.
282: */
283: public String getXPathExpression() {
284: return operator + value;
285: }
286: }
287:
288: /**
289: * A simple NodeList implementation, as simple as it gets. This allows us to not be tied to
290: * any particular XML service provider's NodeList implementation.
291: */
292: private class SimpleNodeList implements NodeList {
293: private List nodes = new ArrayList();
294:
295: public Node item(int index) {
296: return (Node) nodes.get(index);
297: }
298:
299: public int getLength() {
300: return nodes.size();
301: }
302:
303: public List getList() {
304: return nodes;
305: }
306: }
307:
308: public XPath getXpath() {
309: if (this .xpath == null) {
310: return XPathHelper.newXPath();
311: }
312: return xpath;
313: }
314:
315: public void setXpath(XPath xpath) {
316: this.xpath = xpath;
317: }
318:
319: }
|