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 }