Coverage Report - org.kuali.rice.kew.xml.xstream.XStreamSafeEvaluator
 
Classes in this File Line Coverage Branch Coverage Complexity
XStreamSafeEvaluator
0%
0/72
0%
0/38
2.8
XStreamSafeEvaluator$1
N/A
N/A
2.8
XStreamSafeEvaluator$SimpleNodeList
0%
0/5
N/A
2.8
XStreamSafeEvaluator$XPathSegment
0%
0/9
0%
0/2
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  
 }