1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 package org.apache.jmeter.extractor; 19 20 import java.io.ByteArrayInputStream; 21 import java.io.IOException; 22 import java.io.Serializable; 23 import java.io.UnsupportedEncodingException; 24 25 import javax.xml.parsers.ParserConfigurationException; 26 import javax.xml.transform.TransformerException; 27 28 import org.apache.jmeter.assertions.AssertionResult; 29 import org.apache.jmeter.processor.PostProcessor; 30 import org.apache.jmeter.samplers.SampleResult; 31 import org.apache.jmeter.testelement.AbstractTestElement; 32 import org.apache.jmeter.testelement.property.BooleanProperty; 33 import org.apache.jmeter.threads.JMeterContext; 34 import org.apache.jmeter.threads.JMeterVariables; 35 import org.apache.jmeter.util.TidyException; 36 import org.apache.jmeter.util.XPathUtil; 37 import org.apache.jorphan.logging.LoggingManager; 38 import org.apache.jorphan.util.JMeterError; 39 import org.apache.jorphan.util.JOrphanUtils; 40 import org.apache.log.Logger; 41 import org.apache.xpath.XPathAPI; 42 import org.apache.xpath.objects.XObject; 43 import org.w3c.dom.Document; 44 import org.w3c.dom.Element; 45 import org.w3c.dom.Node; 46 import org.w3c.dom.NodeList; 47 import org.xml.sax.SAXException; 48 49 //@see org.apache.jmeter.extractor.TestXPathExtractor for unit tests 50 51 /** 52 * Extracts text from (X)HTML response using XPath query language 53 * Example XPath queries: 54 * <dl> 55 * <dt>/html/head/title</dt> 56 * <dd>extracts Title from HTML response</dd> 57 * <dt>//form[@name='countryForm']//select[@name='country']/option[text()='Czech Republic'])/@value 58 * <dd>extracts value attribute of option element that match text 'Czech Republic' 59 * inside of select element with name attribute 'country' inside of 60 * form with name attribute 'countryForm'</dd> 61 * </dl> 62 */ 63 /* This file is inspired by RegexExtractor. 64 * author <a href="mailto:hpaluch@gitus.cz">Henryk Paluch</a> 65 * of <a href="http://www.gitus.com">Gitus a.s.</a> 66 * 67 * See Bugzilla: 37183 68 */ 69 public class XPathExtractor extends AbstractTestElement implements 70 PostProcessor, Serializable { 71 private static final Logger log = LoggingManager.getLoggerForClass(); 72 private static final String MATCH_NR = "matchNr"; // $NON-NLS-1$ 73 74 //+ JMX file attributes 75 private static final String XPATH_QUERY = "XPathExtractor.xpathQuery"; // $NON-NLS-1$ 76 private static final String REFNAME = "XPathExtractor.refname"; // $NON-NLS-1$ 77 private static final String DEFAULT = "XPathExtractor.default"; // $NON-NLS-1$ 78 private static final String TOLERANT = "XPathExtractor.tolerant"; // $NON-NLS-1$ 79 private static final String NAMESPACE = "XPathExtractor.namespace"; // $NON-NLS-1$ 80 private static final String QUIET = "XPathExtractor.quiet"; // $NON-NLS-1$ 81 private static final String REPORT_ERRORS = "XPathExtractor.report_errors"; // $NON-NLS-1$ 82 private static final String SHOW_WARNINGS = "XPathExtractor.show_warnings"; // $NON-NLS-1$ 83 //- JMX file attributes 84 85 86 private String concat(String s1,String s2){ 87 return new StringBuffer(s1).append("_").append(s2).toString(); // $NON-NLS-1$ 88 } 89 90 /** 91 * Do the job - extract value from (X)HTML response using XPath Query. 92 * Return value as variable defined by REFNAME. Returns DEFAULT value 93 * if not found. 94 */ 95 public void process() { 96 JMeterContext context = getThreadContext(); 97 JMeterVariables vars = context.getVariables(); 98 String refName = getRefName(); 99 vars.put(refName, getDefaultValue()); 100 vars.put(concat(refName,MATCH_NR), "0"); // In case parse fails // $NON-NLS-1$ 101 vars.remove(concat(refName,"1")); // In case parse fails // $NON-NLS-1$ 102 final SampleResult previousResult = context.getPreviousResult(); 103 104 try{ 105 Document d = parseResponse(previousResult); 106 getValuesForXPath(d,getXPathQuery(),vars, refName); 107 }catch(IOException e){// Should not happen 108 final String errorMessage = "error on ("+getXPathQuery()+")"; 109 log.error(errorMessage,e); 110 throw new JMeterError(errorMessage,e); 111 } catch (ParserConfigurationException e) {// Should not happen 112 final String errrorMessage = "error on ("+getXPathQuery()+")"; 113 log.error(errrorMessage,e); 114 throw new JMeterError(errrorMessage,e); 115 } catch (SAXException e) {// Can happen for bad input document 116 log.warn("error on ("+getXPathQuery()+")"+e.getLocalizedMessage()); 117 } catch (TransformerException e) {// Can happen for incorrect XPath expression 118 log.warn("error on ("+getXPathQuery()+")"+e.getLocalizedMessage()); 119 } catch (TidyException e) { 120 AssertionResult ass = new AssertionResult("TidyException"); // $NON-NLS-1$ 121 ass.setFailure(true); 122 ass.setFailureMessage(e.getMessage()); 123 previousResult.addAssertionResult(ass); 124 previousResult.setSuccessful(false); 125 } 126 } 127 128 public Object clone() { 129 XPathExtractor cloned = (XPathExtractor) super.clone(); 130 return cloned; 131 } 132 133 /*============= object properties ================*/ 134 public void setXPathQuery(String val){ 135 setProperty(XPATH_QUERY,val); 136 } 137 138 public String getXPathQuery(){ 139 return getPropertyAsString(XPATH_QUERY); 140 } 141 142 public void setRefName(String refName) { 143 setProperty(REFNAME, refName); 144 } 145 146 public String getRefName() { 147 return getPropertyAsString(REFNAME); 148 } 149 150 public void setDefaultValue(String val) { 151 setProperty(DEFAULT, val); 152 } 153 154 public String getDefaultValue() { 155 return getPropertyAsString(DEFAULT); 156 } 157 158 public void setTolerant(boolean val) { 159 setProperty(new BooleanProperty(TOLERANT, val)); 160 } 161 162 public boolean isTolerant() { 163 return getPropertyAsBoolean(TOLERANT); 164 } 165 166 public void setNameSpace(boolean val) { 167 setProperty(new BooleanProperty(NAMESPACE, val)); 168 } 169 170 public boolean useNameSpace() { 171 return getPropertyAsBoolean(NAMESPACE); 172 } 173 174 public void setReportErrors(boolean val) { 175 setProperty(REPORT_ERRORS, val, false); 176 } 177 178 public boolean reportErrors() { 179 return getPropertyAsBoolean(REPORT_ERRORS, false); 180 } 181 182 public void setShowWarnings(boolean val) { 183 setProperty(SHOW_WARNINGS, val, false); 184 } 185 186 public boolean showWarnings() { 187 return getPropertyAsBoolean(SHOW_WARNINGS, false); 188 } 189 190 public void setQuiet(boolean val) { 191 setProperty(QUIET, val, true); 192 } 193 194 public boolean isQuiet() { 195 return getPropertyAsBoolean(QUIET, true); 196 } 197 198 /*================= internal business =================*/ 199 /** 200 * Converts (X)HTML response to DOM object Tree. 201 * This version cares of charset of response. 202 * @param result 203 * @return 204 * 205 */ 206 private Document parseResponse(SampleResult result) 207 throws UnsupportedEncodingException, IOException, ParserConfigurationException,SAXException,TidyException 208 { 209 //TODO: validate contentType for reasonable types? 210 211 //TODO: is it really necessary to recode the data? 212 // NOTE: responseData encoding is server specific 213 // Therefore we do byte -> unicode -> byte conversion 214 // to ensure UTF-8 encoding as required by XPathUtil 215 String unicodeData = result.getResponseDataAsString(); 216 // convert unicode String -> UTF-8 bytes 217 byte[] utf8data = unicodeData.getBytes("UTF-8"); // $NON-NLS-1$ 218 ByteArrayInputStream in = new ByteArrayInputStream(utf8data); 219 boolean isXML = JOrphanUtils.isXML(utf8data); 220 // this method assumes UTF-8 input data 221 return XPathUtil.makeDocument(in,false,false,useNameSpace(),isTolerant(),isQuiet(),showWarnings(),reportErrors(),isXML); 222 } 223 224 /** 225 * Extract value from Document d by XPath query. 226 * @param d 227 * @param query 228 * @throws TransformerException 229 */ 230 private void getValuesForXPath(Document d,String query, JMeterVariables vars, String refName) 231 throws TransformerException 232 { 233 String val = null; 234 XObject xObject = XPathAPI.eval(d, query); 235 final int objectType = xObject.getType(); 236 if (objectType == XObject.CLASS_NODESET) { 237 NodeList matches = xObject.nodelist(); 238 int length = matches.getLength(); 239 vars.put(concat(refName,MATCH_NR), String.valueOf(length)); 240 for (int i = 0 ; i < length; i++) { 241 Node match = matches.item(i); 242 if ( match instanceof Element){ 243 // elements have empty nodeValue, but we are usually interested in their content 244 final Node firstChild = match.getFirstChild(); 245 if (firstChild != null) { 246 val = firstChild.getNodeValue(); 247 } else { 248 val = match.getNodeValue(); // TODO is this correct? 249 } 250 } else { 251 val = match.getNodeValue(); 252 } 253 if ( val!=null){ 254 if (i==0) {// Treat 1st match specially 255 vars.put(refName,val); 256 } 257 vars.put(concat(refName,String.valueOf(i+1)),val); 258 } 259 } 260 vars.remove(concat(refName,String.valueOf(length+1))); 261 } else if (objectType == XObject.CLASS_NULL 262 || objectType == XObject.CLASS_UNKNOWN 263 || objectType == XObject.CLASS_UNRESOLVEDVARIABLE) { 264 log.warn("Unexpected object type: "+xObject.getTypeString()+" returned for: "+getXPathQuery()); 265 } else { 266 val = xObject.toString(); 267 vars.put(concat(refName, MATCH_NR), "1"); 268 vars.put(refName, val); 269 vars.put(concat(refName, "1"), val); 270 vars.remove(concat(refName, "2")); 271 } 272 } 273 }