001    /*
002     * Copyright 1999-2002,2004 The Apache Software Foundation.
003     * 
004     * Licensed under the Apache 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.apache.org/licenses/LICENSE-2.0
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    
017    package org.apache.commons.latka.validators;
018    
019    import java.io.IOException;
020    
021    import org.apache.commons.latka.ValidationException;
022    import org.apache.commons.latka.http.Response;
023    
024    import org.jdom.Document;
025    import org.jdom.Element;
026    import org.jdom.JDOMException;
027    import org.jdom.input.SAXBuilder;
028    import org.jaxen.jdom.JDOMXPath;
029    import org.jaxen.JaxenException;
030    
031    /**
032     * An XPath validator.
033     *
034     * <p>Use is of the form:</p>
035     * <p>&lt;xpath select="..." [ value="..." ] [ cond="true | false" ] /&gt;</p>
036     * <p>Where :</p>
037     * <ul>
038     *  <li><code>select</code> is an XPath expression, designed to match a node in
039     *  the XML body of the response.</li>
040     *  <li><code>value</code> is an option value which the string value of the
041     *  selected node should match.</li>
042     *  <li><code>cond</code> is an optional boolean value, indicating
043     *  whether the test logic is to be inverted. Defaults to
044     *  <code>true</code>.</li>
045     * </ul>
046     *
047     * </p>
048     * <p>
049     * If the user has specified a {@link #setValue value}, then the XPath
050     * expression is expected to evaluate to a text or attribute node (or other
051     * textual 'leaf' nodes), in which case the selected value must match that
052     * specified.
053     * </p>
054     * <p>
055     * If no value is specified, then the XPath expression will be used to check for
056     * the <em>existence</em> of a node.
057     * </p>
058     * <p>
059     * If the XPath expression evaluates to a boolean condition (eg
060     * &lt;xpath select="foo/bar='baz'"/&gt;), then the condition will be evaluated and will
061     * result in the test passing or failing. Equivalently, an expression may be
062     * specified, and <code>value</code> used to require a specific value (eg
063     * &lt;xpath select="/foo/bar" value="baz"/&gt;).
064     * </p>
065     * <p>
066     * Finally, setting <code>cond="false"</code> negates the sense of the
067     * test, allowing one to test for the <em>nonexistence</em> or
068     * <em>inequality</em> of nodes and values.
069     * </p>
070     *
071     * @author <a href="mailto:jefft@apache.org">Jeff Turner</a>
072     * @author dIon Gillard
073     * @since 6 January, 2001
074     * @version $Id: XPathValidator.java 155424 2005-02-26 13:09:29Z dirkv $
075     */
076    public class XPathValidator extends BaseConditionalValidator {
077    
078        // General notes:
079        // - It started out simple, honest.. =)
080        // --------------------------------------------------------------- Attributes
081    
082        protected String _select = null;
083        protected String _value = null;
084        // need the last XPath result for exception generation
085        protected Object _lastSelected = null;
086    
087        // ------------------------------------------------------------- Constructors
088    
089        public XPathValidator() {
090            this(null,null,true,null);
091        }
092    
093        public XPathValidator(String label) {
094            this(label,null,true,null);
095        }
096    
097        public XPathValidator(String label, String select, boolean cond, String value) {
098            super(label,cond);
099            _select = select;
100            _value = value;
101        }
102    
103        // ------------------------------------------------------------------ Public Methods
104    
105        public void setSelect(String select) {
106            _select = select;
107        }
108    
109        public void setValue(String value) {
110            _value = value;
111        }
112    
113        public boolean assertTrue(Response response)
114        throws ValidationException {
115    
116            JDOMXPath xpath = getJDOMXPath(_select); // compile the XPath expression
117            Document doc = getDocument(response); // retrieve the XML Document to process
118            Object selected = getSelectedNode(xpath, doc); // Apply the XPath to retrieve a node
119            _lastSelected = selected;
120    
121            if (selected == null) {
122                return false;
123            }
124    
125            // Now the fun begins, where we see if our selected node meets the criteria.
126            // There are two factors:
127            // 
128            // 1) What type of object did the XPath expression return?
129            // 2) If _value is specified, ie if we're testing for _value_ or _existence_
130    
131            if (selected instanceof Boolean) {
132                // Eg, we had an expression /foo = 'bar'
133                _log.debug("Boolean XPath expression evaluated to "+selected);
134                if (_value != null) {
135                    _log.warn("Ignoring unused value '"+_value+"'.");
136                }
137                boolean matched = ((Boolean)selected).booleanValue();
138    
139                return matched;
140    
141            } else if (selected instanceof String) {
142                _log.debug("XPath selected string '"+selected+"'.");
143                if (_value != null) {
144                    boolean matched = selected.equals(_value);
145    
146                    return matched;
147                } else {
148                    // otherwise we only test if the node is meant to exist
149                    return true;
150                }
151            } else if (selected instanceof Element) {
152                if (_log.isDebugEnabled()) {
153                    _log.debug("XPath matched element: ");
154                    _log.debug(printElement((Element)selected));
155                }
156                if (_value != null) {
157                    _log.warn("Ignoring unused value '"+_value+"'.");
158                }
159    
160                // otherwise we only test if the node is meant to exist
161                return true;
162            } else {
163                // Otherwise Jaxen is returning something odd
164                if (_value != null) {
165                    // Hope that .equals() does a sensible comparison
166                    boolean matched = selected.equals(_value);
167    
168                    return matched;
169    
170                } else {
171                    _log.warn("Selected unknown type "+selected.getClass().getName());
172                    // only test if the node (whatever it is) is meant to exist
173                    return true;
174                }
175            }
176        }
177    
178        public String generateBareExceptionMessage() {
179    
180            if (_lastSelected == null) {
181                return " THAT BOOLEAN XPATH '"+_select+"' WOULD SELECT SOME NODE.";
182            }
183    
184            if (_lastSelected instanceof Boolean) {
185                return " THAT BOOLEAN XPATH '"+_select+"' WOULD RETURN '" + getCondition() + "'.";
186            } else if (_lastSelected instanceof String) {
187                if (_value != null) {
188                    StringBuffer buf = new StringBuffer();
189                    buf.append(" THAT XPATH '");
190                    buf.append(_select);
191                    buf.append("' WOULD SELECT '");
192                    buf.append(_value);
193                    buf.append("', RECEIVED '");
194                    buf.append(_lastSelected);
195                    buf.append("'.");
196                    return buf.toString();
197                } else {
198                    // otherwise we only test if the node is meant to exist
199                    return " THAT XPATH '" + _select + "' WOULD SELECT SOMETHING.";
200                }
201            } else if (_lastSelected instanceof Element) {
202                    // otherwise we only test if the node is meant to exist
203                    return " THAT XPATH '" + _select + "' WOULD SELECT SOMETHING.";
204            } else {
205                // Otherwise Jaxen is returning something odd
206                if (_value != null) {
207                    return " THAT XPATH EXPRESSION '"+_select+"' WOULD RETURN '" + _value +
208                           "', RETURNED UNKNOWN TYPE "+_lastSelected.getClass().getName()+ ".";
209                } else {
210                    _log.warn("Selected unknown type "+_lastSelected.getClass().getName());
211                    // only test if the node (whatever it is) is meant to exist
212                    return " THAT XPATH '" + _select + "' WOULD SELECT SOMETHING.";
213                }
214            }
215        }
216    
217        // ------------------------------------------------------------------ Private Methods
218    
219        /**
220         * Creates a Jaxen <code>JDOMXPath</code> for a given XPath expression.
221         * @param xpathExpr The XPath expression
222         * @return A non-null Jaxen <code>JDOMXPath</code> object.
223         * @throws ValidationException if <code>xpathExpr</code> was invalid.
224         */
225        private JDOMXPath getJDOMXPath(final String xpathExpr)
226        throws ValidationException
227        {
228            JDOMXPath xpath = null;
229            try {
230                xpath = new JDOMXPath(xpathExpr);
231            } catch (JaxenException e) {
232                fail("Couldn't compile JDOMXPath xpathExpr "+xpathExpr+": "+e.toString());
233            }
234    
235            if (xpath == null) { // this should never happen
236                fail("Null compiled XPath object");
237            }
238    
239            if (_log.isDebugEnabled()) {
240                _log.debug("Using XPath expression: "+xpathExpr);
241            }
242            return xpath;
243        }
244    
245        /**
246         * Creates a <code>Document</code> from the Response.
247         * @param response The (usu. HTTP) Reponse object presumably containing an XML
248         * response body.
249         * @return A non-null <code>Document</code> representing the response body.
250         * @throws ValidationException if the Response object's body did not contain
251         * well-formed XML.
252         */
253        private Document getDocument(Response response)
254        throws ValidationException
255        {
256            Document doc = null;
257            SAXBuilder builder = new SAXBuilder();
258            try {
259                doc = builder.build(response.getStream());
260            } catch (Exception e) {
261                if (e instanceof IOException || e instanceof JDOMException) {
262                    fail(e.toString());
263                } else {
264                    fail("Unknown exception caught: " + e.toString());
265                }
266            }
267            if (doc == null) { // this should never happen
268                fail("Null document");
269            }
270            if (_log.isDebugEnabled()) {
271                _log.debug("Processing doc: "+printDoc(doc));
272            }
273            return doc;
274        }
275    
276        /**
277         * Apply a compiled XPath expression to an XML Document, and return the
278         * selected node. 
279         * @param xpath The compiled Jaxen <code>XPath</code> object
280         * @param doc The <code>Document</code> object containing the XML.
281         * @return An object returned from Jaxen, or null if there was no match. This may be:
282         * <ul>
283         *  <li>A String, if the expression selected an element with a text node child</li>
284         *  <li>An <code>Element</code></li>
285         *  <li>A <code>java.lang.Boolean</code>, if the XPath expression is a
286         *  statement (eg /foo/bar='content')</li>
287         *  <li>Anything else the Jaxen author deemed useful; ie don't assume anything</li>
288         * </ul>
289         */
290        private Object getSelectedNode(JDOMXPath xpath, Document doc)
291        throws ValidationException
292        {
293            Object selected = null;
294            try {
295                selected = xpath.selectSingleNode(doc);
296            } catch (JaxenException e) {
297                fail("XPath expression '"+_select+"' didn't match any node. "+e.toString());
298            }
299    
300            return selected;
301        }
302    
303        /**
304         * Utility method for returning an XML rendition of a <code>Document</code>.
305         * @param doc The Document to print
306         * @return A String of XML representing the document.
307         */
308        private String printDoc(final Document doc) {
309            java.io.StringWriter sw = new java.io.StringWriter();
310            try {
311                new org.jdom.output.XMLOutputter().output(doc, sw);
312            } catch (java.io.IOException ioe) {
313                _log.error("Could not print XML document.", ioe);
314            }
315            return sw.toString();
316        }
317    
318        /**
319         * Utility method for returning an XML rendition of a <code>Element</code>.
320         * @param elem an <code>Element</code> to print.
321         * @return A String of XML representing the element.
322         */
323        private String printElement(final Element elem) {
324            java.io.StringWriter sw = new java.io.StringWriter();
325            Element clone = (Element)((Element)elem).clone();
326            org.jdom.output.XMLOutputter xmlOut = new org.jdom.output.XMLOutputter();
327            try {
328                xmlOut.output(new org.jdom.Document(clone), sw);
329            } catch (java.io.IOException ioe) {
330                _log.error("Could not print XML element.", ioe);
331            }
332            return sw.toString();
333        }
334    }