001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.kew.xml.xstream;
017
018import java.util.ArrayList;
019import java.util.Iterator;
020import java.util.List;
021
022import javax.xml.xpath.XPath;
023import javax.xml.xpath.XPathConstants;
024import javax.xml.xpath.XPathExpressionException;
025
026import org.apache.commons.lang.StringUtils;
027import org.kuali.rice.kew.rule.xmlrouting.XPathHelper;
028import org.w3c.dom.NamedNodeMap;
029import org.w3c.dom.Node;
030import 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 */
052public 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}