| /* |
| * 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); |
| } |
| } |