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