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 }