/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.jmeter.extractor;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.processor.PostProcessor;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testelement.AbstractScopedTestElement;
import org.apache.jmeter.testelement.property.IntegerProperty;
import org.apache.jmeter.threads.JMeterContext;
import org.apache.jmeter.threads.JMeterVariables;
import org.apache.jmeter.util.JMeterUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
*
*/
public class HtmlExtractor extends AbstractScopedTestElement implements PostProcessor, Serializable {
private static final long serialVersionUID = 1L;
public static final String EXTRACTOR_JSOUP = "JSOUP"; //$NON-NLS-1$
public static final String EXTRACTOR_JODD = "JODD"; //$NON-NLS-1$
public static final String DEFAULT_EXTRACTOR = ""; // $NON-NLS-1$
private static final Logger log = LoggerFactory.getLogger(HtmlExtractor.class);
private static final String EXPRESSION = "HtmlExtractor.expr"; // $NON-NLS-1$
private static final String ATTRIBUTE = "HtmlExtractor.attribute"; // $NON-NLS-1$
private static final String REFNAME = "HtmlExtractor.refname"; // $NON-NLS-1$
private static final String MATCH_NUMBER = "HtmlExtractor.match_number"; // $NON-NLS-1$
private static final String DEFAULT = "HtmlExtractor.default"; // $NON-NLS-1$
private static final String EXTRACTOR_IMPL = "HtmlExtractor.extractor_impl"; // $NON-NLS-1$
private static final String REF_MATCH_NR = "_matchNr"; // $NON-NLS-1$
private static final String UNDERSCORE = "_"; // $NON-NLS-1$
private static final String DEFAULT_EMPTY_VALUE = "HtmlExtractor.default_empty_value"; // $NON-NLS-1$
private Extractor extractor;
/**
* Get the possible extractor implementations
* @return Array containing the names of the possible extractors.
*/
public static String[] getImplementations(){
return new String[]{EXTRACTOR_JSOUP,EXTRACTOR_JODD};
}
/**
* Parses the response data using CSS/JQuery expressions and saving the results
* into variables for use later in the test.
*
* @see org.apache.jmeter.processor.PostProcessor#process()
*/
@Override
public void process() {
JMeterContext context = getThreadContext();
SampleResult previousResult = context.getPreviousResult();
if (previousResult == null) {
return;
}
if(log.isDebugEnabled()) {
log.debug("HtmlExtractor {}: processing result", getName());
}
// Fetch some variables
JMeterVariables vars = context.getVariables();
String refName = getRefName();
String expression = getExpression();
String attribute = getAttribute();
int matchNumber = getMatchNumber();
final String defaultValue = getDefaultValue();
if (defaultValue.length() > 0 || isEmptyDefaultValue()){// Only replace default if it is provided or empty default value is explicitly requested
vars.put(refName, defaultValue);
}
try {
List<String> matches =
extractMatchingStrings(vars, expression, attribute, matchNumber, previousResult);
int prevCount = 0;
String prevString = vars.get(refName + REF_MATCH_NR);
if (prevString != null) {
vars.remove(refName + REF_MATCH_NR);// ensure old value is not left defined
try {
prevCount = Integer.parseInt(prevString);
} catch (NumberFormatException nfe) {
if (log.isWarnEnabled()) {
log.warn("{}: Could not parse number: '{}'.", getName(), prevString);
}
}
}
int matchCount=0;// Number of refName_n variable sets to keep
String match;
if (matchNumber >= 0) {// Original match behaviour
match = getCorrectMatch(matches, matchNumber);
if (match != null) {
vars.put(refName, match);
}
} else // < 0 means we save all the matches
{
matchCount = matches.size();
vars.put(refName + REF_MATCH_NR, Integer.toString(matchCount));// Save the count
for (int i = 1; i <= matchCount; i++) {
match = getCorrectMatch(matches, i);
if (match != null) {
final String refNameN = new StringBuilder(refName).append(UNDERSCORE).append(i).toString();
vars.put(refNameN, match);
}
}
}
// Remove any left-over variables
for (int i = matchCount + 1; i <= prevCount; i++) {
final String refNameN = new StringBuilder(refName).append(UNDERSCORE).append(i).toString();
vars.remove(refNameN);
}
} catch (RuntimeException e) {
if (log.isWarnEnabled()) {
log.warn("{}: Error while generating result. {}", getName(), e.toString());
}
}
}
/**
* Grab the appropriate result from the list.
*
* @param matches
* list of matches
* @param entry
* the entry number in the list
* @return MatchResult
*/
private String getCorrectMatch(List<String> matches, int entry) {
int matchSize = matches.size();
if (matchSize <= 0 || entry > matchSize){
return null;
}
if (entry == 0) // Random match
{
return matches.get(JMeterUtils.getRandomInt(matchSize));
}
return matches.get(entry - 1);
}
private List<String> extractMatchingStrings(JMeterVariables vars,
String expression, String attribute, int matchNumber,
SampleResult previousResult) {
int found = 0;
List<String> result = new ArrayList<>();
if (isScopeVariable()){
String inputString=vars.get(getVariableName());
if(!StringUtils.isEmpty(inputString)) {
getExtractorImpl().extract(expression, attribute, matchNumber, inputString, result, found, "-1");
} else {
if(inputString==null) {
if (log.isWarnEnabled()) {
log.warn("No variable '{}' found to process by CSS/JQuery Extractor '{}', skipping processing",
getVariableName(), getName());
}
}
return Collections.emptyList();
}
} else {
List<SampleResult> sampleList = getSampleList(previousResult);
int i=0;
for (SampleResult sr : sampleList) {
String inputString = sr.getResponseDataAsString();
found = getExtractorImpl().extract(expression, attribute, matchNumber, inputString, result, found,
i>0 ? null : Integer.toString(i));
i++;
if (matchNumber > 0 && found == matchNumber){// no need to process further
break;
}
}
}
return result;
}
/**
* @param impl Extractor implementation
* @return Extractor
*/
public static Extractor getExtractorImpl(String impl) {
boolean useDefaultExtractor = DEFAULT_EXTRACTOR.equals(impl);
if (useDefaultExtractor || EXTRACTOR_JSOUP.equals(impl)) {
return new JSoupExtractor();
} else if (EXTRACTOR_JODD.equals(impl)) {
return new JoddExtractor();
} else {
throw new IllegalArgumentException("Extractor implementation:"+ impl+" is unknown");
}
}
/**
*
* @return Extractor
*/
private Extractor getExtractorImpl() {
if (extractor == null) {
extractor = getExtractorImpl(getExtractor());
}
return extractor;
}
/**
* Set the extractor. Has to be one of the list that can be obtained by
* {@link HtmlExtractor#getImplementations()}
*
* @param attribute
* The name of the extractor to be used
*/
public void setExtractor(String attribute) {
setProperty(EXTRACTOR_IMPL, attribute);
}
/**
* Get the name of the currently configured extractor
* @return The name of the extractor currently used
*/
public String getExtractor() {
return getPropertyAsString(EXTRACTOR_IMPL); // $NON-NLS-1$
}
public void setAttribute(String attribute) {
setProperty(ATTRIBUTE, attribute);
}
public String getAttribute() {
return getPropertyAsString(ATTRIBUTE, ""); // $NON-NLS-1$
}
public void setExpression(String regex) {
setProperty(EXPRESSION, regex);
}
public String getExpression() {
return getPropertyAsString(EXPRESSION);
}
public void setRefName(String refName) {
setProperty(REFNAME, refName);
}
public String getRefName() {
return getPropertyAsString(REFNAME);
}
/**
* Set which Match to use. This can be any positive number, indicating the
* exact match to use, or <code>0</code>, which is interpreted as meaning random.
*
* @param matchNumber The number of the match to be used
*/
public void setMatchNumber(int matchNumber) {
setProperty(new IntegerProperty(MATCH_NUMBER, matchNumber));
}
public void setMatchNumber(String matchNumber) {
setProperty(MATCH_NUMBER, matchNumber);
}
public int getMatchNumber() {
return getPropertyAsInt(MATCH_NUMBER);
}
public String getMatchNumberAsString() {
return getPropertyAsString(MATCH_NUMBER);
}
/**
* Sets the value of the variable if no matches are found
*
* @param defaultValue The default value for the variable
*/
public void setDefaultValue(String defaultValue) {
setProperty(DEFAULT, defaultValue);
}
/**
* @param defaultEmptyValue boolean set value to "" if not found
*/
public void setDefaultEmptyValue(boolean defaultEmptyValue) {
setProperty(DEFAULT_EMPTY_VALUE, defaultEmptyValue);
}
/**
* Get the default value for the variable if no matches are found
* @return The default value for the variable
*/
public String getDefaultValue() {
return getPropertyAsString(DEFAULT);
}
/**
* @return boolean set value to "" if not found
*/
public boolean isEmptyDefaultValue() {
return getPropertyAsBoolean(DEFAULT_EMPTY_VALUE);
}
}