001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018: package org.apache.jmeter.extractor;
019:
020: import java.io.ByteArrayInputStream;
021: import java.io.IOException;
022: import java.io.Serializable;
023: import java.io.UnsupportedEncodingException;
024:
025: import javax.xml.parsers.ParserConfigurationException;
026: import javax.xml.transform.TransformerException;
027:
028: import org.apache.jmeter.processor.PostProcessor;
029: import org.apache.jmeter.samplers.SampleResult;
030: import org.apache.jmeter.testelement.AbstractTestElement;
031: import org.apache.jmeter.testelement.property.BooleanProperty;
032: import org.apache.jmeter.threads.JMeterContext;
033: import org.apache.jmeter.threads.JMeterVariables;
034: import org.apache.jmeter.util.XPathUtil;
035: import org.apache.jorphan.logging.LoggingManager;
036: import org.apache.jorphan.util.JMeterError;
037: import org.apache.log.Logger;
038: import org.apache.xpath.XPathAPI;
039: import org.apache.xpath.objects.XObject;
040: import org.w3c.dom.Document;
041: import org.w3c.dom.Element;
042: import org.w3c.dom.Node;
043: import org.w3c.dom.NodeList;
044: import org.xml.sax.SAXException;
045:
046: //@see org.apache.jmeter.extractor.TestXPathExtractor for unit tests
047:
048: /**
049: * Extracts text from (X)HTML response using XPath query language
050: * Example XPath queries:
051: * <dl>
052: * <dt>/html/head/title</dt>
053: * <dd>extracts Title from HTML response</dd>
054: * <dt>//form[@name='countryForm']//select[@name='country']/option[text()='Czech Republic'])/@value
055: * <dd>extracts value attribute of option element that match text 'Czech Republic'
056: * inside of select element with name attribute 'country' inside of
057: * form with name attribute 'countryForm'</dd>
058: * </dl>
059: */
060: /* This file is inspired by RegexExtractor.
061: * author <a href="mailto:hpaluch@gitus.cz">Henryk Paluch</a>
062: * of <a href="http://www.gitus.com">Gitus a.s.</a>
063: *
064: * See Bugzilla: 37183
065: */
066: public class XPathExtractor extends AbstractTestElement implements
067: PostProcessor, Serializable {
068: private static final Logger log = LoggingManager
069: .getLoggerForClass();
070: private static final String MATCH_NR = "matchNr"; // $NON-NLS-1$
071: protected static final String KEY_PREFIX = "XPathExtractor."; // $NON-NLS-1$
072: private static final String XPATH_QUERY = KEY_PREFIX + "xpathQuery"; // $NON-NLS-1$
073: private static final String REFNAME = KEY_PREFIX + "refname"; // $NON-NLS-1$
074: private static final String DEFAULT = KEY_PREFIX + "default"; // $NON-NLS-1$
075: private static final String TOLERANT = KEY_PREFIX + "tolerant"; // $NON-NLS-1$
076: private static final String NAMESPACE = KEY_PREFIX + "namespace"; // $NON-NLS-1$
077:
078: private String concat(String s1, String s2) {
079: return new StringBuffer(s1).append("_").append(s2).toString(); // $NON-NLS-1$
080: }
081:
082: /**
083: * Do the job - extract value from (X)HTML response using XPath Query.
084: * Return value as variable defined by REFNAME. Returns DEFAULT value
085: * if not found.
086: */
087: public void process() {
088: JMeterContext context = getThreadContext();
089: JMeterVariables vars = context.getVariables();
090: String refName = getRefName();
091: vars.put(refName, getDefaultValue());
092: vars.put(concat(refName, MATCH_NR), "0"); // In case parse fails // $NON-NLS-1$
093: vars.remove(concat(refName, "1")); // In case parse fails // $NON-NLS-1$
094:
095: try {
096: Document d = parseResponse(context.getPreviousResult());
097: getValuesForXPath(d, getXPathQuery(), vars, refName);
098: } catch (IOException e) {// Should not happen
099: final String errorMessage = "error on " + XPATH_QUERY + "("
100: + getXPathQuery() + ")";
101: log.error(errorMessage, e);
102: throw new JMeterError(errorMessage, e);
103: } catch (ParserConfigurationException e) {// Should not happen
104: final String errrorMessage = "error on " + XPATH_QUERY
105: + "(" + getXPathQuery() + ")";
106: log.error(errrorMessage, e);
107: throw new JMeterError(errrorMessage, e);
108: } catch (SAXException e) {// Can happen for bad input document
109: log.warn("error on " + XPATH_QUERY + "(" + getXPathQuery()
110: + ")" + e.getLocalizedMessage());
111: } catch (TransformerException e) {// Can happen for incorrect XPath expression
112: log.warn("error on " + XPATH_QUERY + "(" + getXPathQuery()
113: + ")" + e.getLocalizedMessage());
114: }
115: }
116:
117: /**
118: * Clone?
119: */
120: public Object clone() {
121: XPathExtractor cloned = (XPathExtractor) super .clone();
122: return cloned;
123: }
124:
125: /*============= object properties ================*/
126: public void setXPathQuery(String val) {
127: setProperty(XPATH_QUERY, val);
128: }
129:
130: public String getXPathQuery() {
131: return getPropertyAsString(XPATH_QUERY);
132: }
133:
134: public void setRefName(String refName) {
135: setProperty(REFNAME, refName);
136: }
137:
138: public String getRefName() {
139: return getPropertyAsString(REFNAME);
140: }
141:
142: public void setDefaultValue(String val) {
143: setProperty(DEFAULT, val);
144: }
145:
146: public String getDefaultValue() {
147: return getPropertyAsString(DEFAULT);
148: }
149:
150: public void setTolerant(boolean val) {
151: setProperty(new BooleanProperty(TOLERANT, val));
152: }
153:
154: public boolean isTolerant() {
155: return getPropertyAsBoolean(TOLERANT);
156: }
157:
158: public void setNameSpace(boolean val) {
159: setProperty(new BooleanProperty(NAMESPACE, val));
160: }
161:
162: public boolean useNameSpace() {
163: return getPropertyAsBoolean(NAMESPACE);
164: }
165:
166: /*================= internal business =================*/
167: /**
168: * Converts (X)HTML response to DOM object Tree.
169: * This version cares of charset of response.
170: * @param result
171: * @return
172: *
173: */
174: private Document parseResponse(SampleResult result)
175: throws UnsupportedEncodingException, IOException,
176: ParserConfigurationException, SAXException {
177: //TODO: validate contentType for reasonable types?
178:
179: //TODO: is it really necessary to recode the data?
180: // NOTE: responseData encoding is server specific
181: // Therefore we do byte -> unicode -> byte conversion
182: // to ensure UTF-8 encoding as required by XPathUtil
183: String unicodeData = result.getResponseDataAsString();
184: // convert unicode String -> UTF-8 bytes
185: byte[] utf8data = unicodeData.getBytes("UTF-8"); // $NON-NLS-1$
186: ByteArrayInputStream in = new ByteArrayInputStream(utf8data);
187: // this method assumes UTF-8 input data
188: return XPathUtil.makeDocument(in, false, false, useNameSpace(),
189: isTolerant());
190: }
191:
192: /**
193: * Extract value from Document d by XPath query.
194: * @param d
195: * @param query
196: * @throws TransformerException
197: */
198: private void getValuesForXPath(Document d, String query,
199: JMeterVariables vars, String refName)
200: throws TransformerException {
201: String val = null;
202: XObject xObject = XPathAPI.eval(d, query);
203: if (xObject.getType() == XObject.CLASS_NODESET) {
204: NodeList matches = xObject.nodelist();
205: int length = matches.getLength();
206: vars.put(concat(refName, MATCH_NR), String.valueOf(length));
207: for (int i = 0; i < length; i++) {
208: Node match = matches.item(i);
209: if (match instanceof Element) {
210: // elements have empty nodeValue, but we are usually interested in their content
211: final Node firstChild = match.getFirstChild();
212: if (firstChild != null) {
213: val = firstChild.getNodeValue();
214: } else {
215: val = match.getNodeValue(); // TODO is this correct?
216: }
217: } else {
218: val = match.getNodeValue();
219: }
220: if (val != null) {
221: if (i == 0) {// Treat 1st match specially
222: vars.put(refName, val);
223: }
224: vars.put(concat(refName, String.valueOf(i + 1)),
225: val);
226: }
227: }
228: vars.remove(concat(refName, String.valueOf(length + 1)));
229: } else {
230: val = xObject.toString();
231: vars.put(concat(refName, MATCH_NR), "1");
232: vars.put(refName, val);
233: vars.put(concat(refName, "1"), val);
234: vars.remove(concat(refName, "2"));
235: }
236: }
237: }
|