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    }