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><xpath select="..." [ value="..." ] [ cond="true | false" ] /></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 * <xpath select="foo/bar='baz'"/>), 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 * <xpath select="/foo/bar" value="baz"/>). 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 }