blob: ac64abe41fd6e0c665ed0e573b618340d6cf793c [file] [log] [blame]
/*
* 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 java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
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.Document;
import org.apache.jmeter.util.JMeterUtils;
import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Extracts Strings from a text response between a start and end boundary.
*/
public class BoundaryExtractor extends AbstractScopedTestElement implements PostProcessor, Serializable {
private static final Logger log = LoggerFactory.getLogger(BoundaryExtractor.class);
private static final long serialVersionUID = 2L;
private static final String REFNAME = "BoundaryExtractor.refname"; // $NON-NLS-1$
private static final String MATCH_NUMBER = "BoundaryExtractor.match_number"; // $NON-NLS-1$
private static final String L_BOUNDARY = "BoundaryExtractor.lboundary"; // $NON-NLS-1$
private static final String R_BOUNDARY = "BoundaryExtractor.rboundary"; // $NON-NLS-1$
private static final String DEFAULT_EMPTY_VALUE = "BoundaryExtractor.default_empty_value"; // $NON-NLS-1$
private static final String DEFAULT = "BoundaryExtractor.default"; // $NON-NLS-1$
private static final String REF_MATCH_NR = "_matchNr"; // $NON-NLS-1$
private static final char UNDERSCORE = '_'; // $NON-NLS-1$
// What to match against. N.B. do not change the string value or test plans will break!
private static final String MATCH_AGAINST = "BoundaryExtractor.useHeaders"; // $NON-NLS-1$
/*
* Permissible values:
* true - match against headers
* false or absent - match against body (this was the original default)
* URL - match against URL
* These are passed to the setUseField() method
*
* Do not change these values!
*/
private static final String USE_HDRS = "true"; // $NON-NLS-1$
private static final String USE_REQUEST_HDRS = "request_headers"; // $NON-NLS-1$
private static final String USE_BODY = "false"; // $NON-NLS-1$
private static final String USE_BODY_UNESCAPED = "unescaped"; // $NON-NLS-1$
private static final String USE_BODY_AS_DOCUMENT = "as_document"; // $NON-NLS-1$
private static final String USE_URL = "URL"; // $NON-NLS-1$
private static final String USE_CODE = "code"; // $NON-NLS-1$
private static final String USE_MESSAGE = "message"; // $NON-NLS-1$
/**
* Parses the response data using Boundaries and saving the results
* into variables for use later in the test.
*
* @see PostProcessor#process()
*/
@Override
public void process() {
JMeterContext context = getThreadContext();
SampleResult previousResult = context.getPreviousResult();
if (previousResult == null) {
return;
}
if (log.isDebugEnabled()) {
log.debug("Boundary Extractor {}: processing result", getName());
}
if (StringUtils.isEmpty(getRefName())) {
throw new IllegalArgumentException(
"One of the mandatory properties is missing in Boundary Extractor:" + getName());
}
JMeterVariables vars = context.getVariables();
String refName = getRefName();
final String defaultValue = getDefaultValue();
if (StringUtils.isNotBlank(defaultValue) || isEmptyDefaultValue()) {
vars.put(refName, defaultValue);
}
int matchNumber = getMatchNumber();
int prevCount = 0;
int matchCount = 0;
try {
prevCount = removePrevCount(vars, refName);
List<String> matches = extractMatches(previousResult, vars, matchNumber);
matchCount = saveMatches(vars, refName, matchNumber, matches);
} catch (RuntimeException e) { // NOSONAR
if (log.isWarnEnabled()) {
log.warn("{}: Error while generating result. {}", getName(), e.toString()); // NOSONAR We don't want to be too verbose
}
} finally {
// Remove any left-over variables
for (int i = matchCount + 1; i <= prevCount; i++) {
vars.remove(refName + UNDERSCORE + i);
}
}
}
private int removePrevCount(JMeterVariables vars, String refName) {
int prevCount = 0;
String prevString = vars.get(refName + REF_MATCH_NR);
if (prevString != null) {
// ensure old value is not left defined
vars.remove(refName + REF_MATCH_NR);
try {
prevCount = Integer.parseInt(prevString);
} catch (NumberFormatException nfe) {
if (log.isWarnEnabled()) {
log.warn("{}: Could not parse number: '{}'.", getName(), prevString);
}
}
}
return prevCount;
}
private List<String> extractMatches(SampleResult previousResult, JMeterVariables vars, int matchNumber) {
if (isScopeVariable()) {
String inputString = vars.get(getVariableName());
if (inputString == null && log.isWarnEnabled()) {
log.warn("No variable '{}' found to process by Boundary Extractor '{}', skipping processing",
getVariableName(), getName());
}
return extract(getLeftBoundary(), getRightBoundary(), matchNumber, inputString);
} else {
Stream<String> inputs = getSampleList(previousResult).stream().map(this::getInputString);
return extract(getLeftBoundary(), getRightBoundary(), matchNumber, inputs);
}
}
/**
* @param vars {@link JMeterVariables}
* @param refName Var name
* @param matchNumber number of matches
* @param matches List of String
* @return 0 if there is only one match, else the number of matches, this is used to remove
*/
private static int saveMatches(JMeterVariables vars, String refName, int matchNumber, List<String> matches) {
if (matchNumber >=0 && matches.isEmpty()) {
return 0;
}
int matchCount = 0;
if (matchNumber == 0) {
saveRandomMatch(vars, refName, matches);
} else if (matchNumber > 0) {
saveOneMatch(vars, refName, matches);
} else {
matchCount = matches.size();
saveAllMatches(vars, refName, matches);
}
return matchCount;
}
private static void saveRandomMatch(JMeterVariables vars, String refName, List<String> matches) {
String match = matches.get(JMeterUtils.getRandomInt(matches.size()));
if (match != null) {
vars.put(refName, match);
}
}
private static void saveOneMatch(JMeterVariables vars, String refName, List<String> matches) {
if (matches.size() == 1) { // if not then invalid matchNum was likely supplied
String match = matches.get(0);
if (match != null) {
vars.put(refName, match);
}
}
}
private static void saveAllMatches(JMeterVariables vars, String refName, List<String> matches) {
vars.put(refName + REF_MATCH_NR, Integer.toString(matches.size()));
for (int i = 0; i < matches.size(); i++) {
String match = matches.get(i);
if (match != null) {
int varNum = i + 1;
vars.put(refName + UNDERSCORE + varNum, match);
}
}
}
private String getInputString(SampleResult result) {
String inputString = chosenInput(result);
log.debug("Input = '{}'", inputString);
return inputString;
}
private String chosenInput(SampleResult result) {
if (useUrl()) {
return result.getUrlAsString(); // Bug 39707;
}
if (useHeaders()) {
return result.getResponseHeaders();
}
if (useRequestHeaders()) {
return result.getRequestHeaders();
}
if (useCode()) {
return result.getResponseCode(); // Bug 43451
}
if (useMessage()) {
return result.getResponseMessage(); // Bug 43451
}
if (useUnescapedBody()) {
return StringEscapeUtils.unescapeHtml4(result.getResponseDataAsString());
}
if (useBodyAsDocument()) {
return Document.getTextFromDocument(result.getResponseData());
}
return result.getResponseDataAsString(); // Bug 36898
}
@VisibleForTesting
static List<String> extract(
String leftBoundary, String rightBoundary, int matchNumber, Stream<String> previousResults) {
boolean allItems = matchNumber <= 0;
return previousResults
.flatMap(input -> extract(leftBoundary, rightBoundary, input).stream())
.skip(allItems ? 0L : matchNumber - 1)
.limit(allItems ? Long.MAX_VALUE : 1L)
.collect(Collectors.toList());
}
/**
* Extracts text fragments, that are between the boundaries, into {@code result}.
* The number of extracted fragments can be controlled by {@code matchNumber}
*
* @param leftBoundary fragment representing the left boundary of the searched text
* @param rightBoundary fragment representing the right boundary of the searched text
* @param matchNumber if {@code <=0}, all found matches will be returned, else only
* up to {@code matchNumber} matches
* @param inputString text in which to look for the fragments
* @return list where the found text fragments will be placed
*/
@VisibleForTesting
static List<String> extract(String leftBoundary, String rightBoundary, int matchNumber, String inputString) {
if (StringUtils.isBlank(inputString)) {
return Collections.emptyList();
}
boolean isEmptyLeftBoundary = StringUtils.isEmpty(leftBoundary);
boolean isEmptyRightBoundary = StringUtils.isEmpty(rightBoundary);
if (isEmptyLeftBoundary && isEmptyRightBoundary) {
return Collections.singletonList(inputString);
}
if (isEmptyLeftBoundary) {
int rightBoundaryIndex = inputString.indexOf(rightBoundary);
if (rightBoundaryIndex != -1) {
return Collections.singletonList(inputString.substring(0, rightBoundaryIndex));
}
}
if (isEmptyRightBoundary) {
int leftBoundaryIndex = inputString.indexOf(leftBoundary);
if (leftBoundaryIndex != -1) {
return Collections.singletonList(inputString.substring(leftBoundaryIndex + leftBoundary.length()));
}
}
List<String> matches = new ArrayList<>();
int leftBoundaryLen = leftBoundary.length();
boolean collectAll = matchNumber <= 0;
int found = 0;
for (int startIndex = 0;
(startIndex = inputString.indexOf(leftBoundary, startIndex)) != -1;
startIndex += leftBoundaryLen) {
int endIndex = inputString.indexOf(rightBoundary, startIndex + leftBoundaryLen);
if (endIndex >= 0) {
found++;
if (collectAll) {
matches.add(inputString.substring(startIndex + leftBoundaryLen, endIndex));
} else if (found == matchNumber) {
return Collections.singletonList(inputString.substring(startIndex + leftBoundaryLen, endIndex));
}
} else {
break;
}
}
return Collections.unmodifiableList(matches);
}
public List<String> extractAll(
String leftBoundary, String rightBoundary, String textToParse) {
return extract(leftBoundary, rightBoundary, textToParse);
}
private static List<String> extract(
String leftBoundary, String rightBoundary, String textToParse) {
return extract(leftBoundary, rightBoundary, -1, textToParse);
}
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);
}
public void setLeftBoundary(String leftBoundary) {
setProperty(L_BOUNDARY, leftBoundary);
}
public String getLeftBoundary() {
return getPropertyAsString(L_BOUNDARY);
}
public void setRightBoundary(String rightBoundary) {
setProperty(R_BOUNDARY, rightBoundary);
}
public String getRightBoundary() {
return getPropertyAsString(R_BOUNDARY);
}
/**
* 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);
}
public boolean useHeaders() {
return USE_HDRS.equalsIgnoreCase(getPropertyAsString(MATCH_AGAINST));
}
public boolean useRequestHeaders() {
return USE_REQUEST_HDRS.equalsIgnoreCase(getPropertyAsString(MATCH_AGAINST));
}
public boolean useBody() {
String prop = getPropertyAsString(MATCH_AGAINST);
return prop.length() == 0 || USE_BODY.equalsIgnoreCase(prop);
}
public boolean useUnescapedBody() {
String prop = getPropertyAsString(MATCH_AGAINST);
return USE_BODY_UNESCAPED.equalsIgnoreCase(prop);
}
public boolean useBodyAsDocument() {
String prop = getPropertyAsString(MATCH_AGAINST);
return USE_BODY_AS_DOCUMENT.equalsIgnoreCase(prop);
}
public boolean useUrl() {
String prop = getPropertyAsString(MATCH_AGAINST);
return USE_URL.equalsIgnoreCase(prop);
}
public boolean useCode() {
String prop = getPropertyAsString(MATCH_AGAINST);
return USE_CODE.equalsIgnoreCase(prop);
}
public boolean useMessage() {
String prop = getPropertyAsString(MATCH_AGAINST);
return USE_MESSAGE.equalsIgnoreCase(prop);
}
public void setUseField(String actionCommand) {
setProperty(MATCH_AGAINST, actionCommand);
}
}