You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jmeter.apache.org by pm...@apache.org on 2017/10/18 20:11:10 UTC

svn commit: r1812568 - in /jmeter/trunk: bin/ src/components/org/apache/jmeter/extractor/ src/components/org/apache/jmeter/extractor/gui/ test/src/org/apache/jmeter/extractor/ xdocs/ xdocs/usermanual/

Author: pmouawad
Date: Wed Oct 18 20:11:09 2017
New Revision: 1812568

URL: http://svn.apache.org/viewvc?rev=1812568&view=rev
Log:
Bug 60213 - Boundary based extractor
Bugzilla Id: 60213

Added:
    jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java   (with props)
    jmeter/trunk/src/components/org/apache/jmeter/extractor/gui/BoundaryExtractorGui.java   (with props)
    jmeter/trunk/test/src/org/apache/jmeter/extractor/TestBoundaryExtractor.java   (with props)
Modified:
    jmeter/trunk/bin/saveservice.properties
    jmeter/trunk/xdocs/changes.xml
    jmeter/trunk/xdocs/usermanual/component_reference.xml

Modified: jmeter/trunk/bin/saveservice.properties
URL: http://svn.apache.org/viewvc/jmeter/trunk/bin/saveservice.properties?rev=1812568&r1=1812567&r2=1812568&view=diff
==============================================================================
--- jmeter/trunk/bin/saveservice.properties (original)
+++ jmeter/trunk/bin/saveservice.properties Wed Oct 18 20:11:09 2017
@@ -64,7 +64,8 @@
 # 2.9 = 2.14
 # 3.1 = 3.1
 # 3.2 = 3.2
-_version=3.2
+# 3.4 = 3.4
+_version=3.4
 #
 #
 # Character set encoding used to read and write JMeter XML files and CSV results
@@ -99,6 +100,8 @@ BeanShellPreProcessor=org.apache.jmeter.
 BeanShellSampler=org.apache.jmeter.protocol.java.sampler.BeanShellSampler
 BeanShellSamplerGui=org.apache.jmeter.protocol.java.control.gui.BeanShellSamplerGui
 BeanShellTimer=org.apache.jmeter.timers.BeanShellTimer
+BoundaryExtractor=org.apache.jmeter.extractor.BoundaryExtractor
+BoundaryExtractorGui=org.apache.jmeter.extractor.gui.BoundaryExtractorGui
 BSFAssertion=org.apache.jmeter.assertions.BSFAssertion
 BSFListener=org.apache.jmeter.visualizers.BSFListener
 BSFPreProcessor=org.apache.jmeter.modifiers.BSFPreProcessor

Added: jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java?rev=1812568&view=auto
==============================================================================
--- jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java (added)
+++ jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java Wed Oct 18 20:11:09 2017
@@ -0,0 +1,393 @@
+/*
+ * 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.StringEscapeUtils;
+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.Document;
+import org.apache.jmeter.util.JMeterUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 
+ */
+public class BoundaryExtractor extends AbstractScopedTestElement implements PostProcessor, Serializable {
+
+    private static final Logger log = LoggerFactory.getLogger(BoundaryExtractor.class);
+
+    private static final long serialVersionUID = 1L;
+
+    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 String 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!
+    */
+    public static final String USE_HDRS = "true"; // $NON-NLS-1$
+    public static final String USE_REQUEST_HDRS = "request_headers"; // $NON-NLS-1$
+    public static final String USE_BODY = "false"; // $NON-NLS-1$
+    public static final String USE_BODY_UNESCAPED = "unescaped"; // $NON-NLS-1$
+    public static final String USE_BODY_AS_DOCUMENT = "as_document"; // $NON-NLS-1$
+    public static final String USE_URL = "URL"; // $NON-NLS-1$
+    public static final String USE_CODE = "code"; // $NON-NLS-1$
+    public 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 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("Boundary Extractor {}: processing result", getName());
+        }
+        if(StringUtils.isEmpty(getLeftBoundary()) ||
+                StringUtils.isEmpty(getRightBoundary()) ||
+                StringUtils.isEmpty(getRefName())
+                ) {
+            throw new IllegalArgumentException("One of the mandatory properties is missing in Boundary Extractor:"+
+                getName());
+        }
+        // Fetch some variables
+        JMeterVariables vars = context.getVariables();
+        
+        String refName = getRefName();
+        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, getLeftBoundary(), getRightBoundary(), 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());
+            }
+        }
+    }
+
+    private String getInputString(SampleResult result) {
+        String inputString = useUrl() ? result.getUrlAsString() // Bug 39707
+                : useHeaders() ? result.getResponseHeaders()
+                : useRequestHeaders() ? result.getRequestHeaders()
+                : useCode() ? result.getResponseCode() // Bug 43451
+                : useMessage() ? result.getResponseMessage() // Bug 43451
+                : useUnescapedBody() ? StringEscapeUtils.unescapeHtml4(result.getResponseDataAsString())
+                : useBodyAsDocument() ? Document.getTextFromDocument(result.getResponseData())
+                : result.getResponseDataAsString() // Bug 36898
+                ;
+       log.debug("Input = '{}'", inputString);
+       return inputString;
+    }
+    /**
+     * 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 leftBoundary, String rightBoundary, int matchNumber,
+            SampleResult previousResult) {
+        int found = 0;
+        List<String> result = new ArrayList<>();
+        if (isScopeVariable()){
+            String inputString=vars.get(getVariableName());
+            if(!StringUtils.isEmpty(inputString)) {
+                extract(leftBoundary, rightBoundary, matchNumber, inputString, result, found);
+            } else {
+                if(inputString==null) {
+                    if (log.isWarnEnabled()) {
+                        log.warn("No variable '{}' found to process by Boundary Extractor '{}', skipping processing",
+                                getVariableName(), getName());
+                    }
+                }
+                return Collections.emptyList();
+            } 
+        } else {
+            List<SampleResult> sampleList = getSampleList(previousResult);
+            for (SampleResult sr : sampleList) {
+                String inputString = getInputString(sr);
+                found = extract(leftBoundary, rightBoundary, matchNumber, inputString, result, found);
+                if (matchNumber > 0 && found == matchNumber){// no need to process further
+                    break;
+                }
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 
+     * @param leftBoundary
+     * @param rightBoundary
+     * @param matchNumber
+     * @param inputString
+     * @param result
+     * @param found
+     * @return int found updated
+     */
+    private int extract(String leftBoundary, String rightBoundary, int matchNumber, String inputString,
+            List<String> result, int found) {
+        int startIndex = -1;
+        int endIndex = -1;
+        List<String> matches = new ArrayList<>();
+        while(true) {
+            startIndex = inputString.indexOf(leftBoundary, startIndex+1);
+            if(startIndex >= 0) {
+                endIndex = inputString.indexOf(rightBoundary, startIndex+leftBoundary.length());
+                if(endIndex >= 0) {
+                    result.add(inputString.substring(startIndex+leftBoundary.length(), endIndex));
+                } else {
+                    result.add(inputString.substring(startIndex+leftBoundary.length()));
+                    break;
+                }
+            } else {
+                break;
+            }
+        }
+        
+        for (String element : matches) {
+            if (matchNumber <= 0 || found != matchNumber) {
+                result.add(element);
+                found++;
+            } else {
+                break;
+            }
+        }
+        return found;
+    }
+
+    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));
+    }
+
+    // Allow for property not yet being set (probably only applies to Test cases)
+    public boolean useBody() {
+        String prop = getPropertyAsString(MATCH_AGAINST);
+        return prop.length()==0 || USE_BODY.equalsIgnoreCase(prop);// $NON-NLS-1$
+    }
+
+    public boolean useUnescapedBody() {
+        String prop = getPropertyAsString(MATCH_AGAINST);
+        return USE_BODY_UNESCAPED.equalsIgnoreCase(prop);// $NON-NLS-1$
+    }
+
+    public boolean useBodyAsDocument() {
+        String prop = getPropertyAsString(MATCH_AGAINST);
+        return USE_BODY_AS_DOCUMENT.equalsIgnoreCase(prop);// $NON-NLS-1$
+    }
+
+    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);
+    }
+}

Propchange: jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/src/components/org/apache/jmeter/extractor/BoundaryExtractor.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: jmeter/trunk/src/components/org/apache/jmeter/extractor/gui/BoundaryExtractorGui.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/src/components/org/apache/jmeter/extractor/gui/BoundaryExtractorGui.java?rev=1812568&view=auto
==============================================================================
--- jmeter/trunk/src/components/org/apache/jmeter/extractor/gui/BoundaryExtractorGui.java (added)
+++ jmeter/trunk/src/components/org/apache/jmeter/extractor/gui/BoundaryExtractorGui.java Wed Oct 18 20:11:09 2017
@@ -0,0 +1,283 @@
+/*
+ * 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.gui;
+
+import java.awt.BorderLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.ButtonGroup;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+
+import org.apache.jmeter.extractor.BoundaryExtractor;
+import org.apache.jmeter.extractor.RegexExtractor;
+import org.apache.jmeter.processor.gui.AbstractPostProcessorGui;
+import org.apache.jmeter.testelement.AbstractScopedTestElement;
+import org.apache.jmeter.testelement.TestElement;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jorphan.gui.JLabeledTextField;
+
+/**
+ * Boundary Extractor Post-Processor GUI
+ * @since 4.0
+ */
+public class BoundaryExtractorGui extends AbstractPostProcessorGui {
+    private static final long serialVersionUID = 240L;
+
+    private JLabeledTextField leftBoundaryField;
+
+    private JLabeledTextField rightBoundaryField;
+    
+    private JLabeledTextField defaultField;
+
+    private JLabeledTextField matchNumberField;
+
+    private JLabeledTextField refNameField;
+
+    private JCheckBox emptyDefaultValue;
+    
+    private JRadioButton useBody;
+
+    private JRadioButton useUnescapedBody;
+
+    private JRadioButton useBodyAsDocument;
+
+    private JRadioButton useHeaders;
+
+    private JRadioButton useRequestHeaders;
+
+    private JRadioButton useURL;
+
+    private JRadioButton useCode;
+
+    private JRadioButton useMessage;
+
+    private ButtonGroup group;
+    
+    public BoundaryExtractorGui() {
+        super();
+        init();
+    }
+
+    @Override
+    public String getLabelResource() {
+        return "boundaryextractor_title"; //$NON-NLS-1$
+    }
+
+    @Override
+    public void configure(TestElement el) {
+        super.configure(el);
+        if (el instanceof BoundaryExtractor){
+            BoundaryExtractor boundary = (BoundaryExtractor) el;
+            showScopeSettings(boundary, true);
+            useHeaders.setSelected(boundary.useHeaders());
+            useRequestHeaders.setSelected(boundary.useRequestHeaders());
+            useBody.setSelected(boundary.useBody());
+            useUnescapedBody.setSelected(boundary.useUnescapedBody());
+            useBodyAsDocument.setSelected(boundary.useBodyAsDocument());
+            useURL.setSelected(boundary.useUrl());
+            useCode.setSelected(boundary.useCode());
+            useMessage.setSelected(boundary.useMessage());
+            leftBoundaryField.setText(boundary.getLeftBoundary());
+            rightBoundaryField.setText(boundary.getRightBoundary());
+            defaultField.setText(boundary.getDefaultValue());
+            emptyDefaultValue.setSelected(boundary.isEmptyDefaultValue());
+            matchNumberField.setText(boundary.getMatchNumberAsString());
+            refNameField.setText(boundary.getRefName());
+        }
+    }
+
+    /**
+     * @see org.apache.jmeter.gui.JMeterGUIComponent#createTestElement()
+     */
+    @Override
+    public TestElement createTestElement() {
+        AbstractScopedTestElement extractor = new BoundaryExtractor();
+        modifyTestElement(extractor);
+        return extractor;
+    }
+
+    /**
+     * Modifies a given TestElement to mirror the data in the gui components.
+     *
+     * @see org.apache.jmeter.gui.JMeterGUIComponent#modifyTestElement(TestElement)
+     */
+    @Override
+    public void modifyTestElement(TestElement extractor) {
+        super.configureTestElement(extractor);
+        if (extractor instanceof BoundaryExtractor) {
+            BoundaryExtractor boundary = (BoundaryExtractor) extractor;
+            saveScopeSettings(boundary);
+            boundary.setUseField(group.getSelection().getActionCommand());
+            boundary.setRefName(refNameField.getText());
+            boundary.setLeftBoundary(leftBoundaryField.getText());
+            boundary.setRightBoundary(rightBoundaryField.getText());
+            boundary.setDefaultValue(defaultField.getText());
+            boundary.setDefaultEmptyValue(emptyDefaultValue.isSelected());
+            boundary.setMatchNumber(matchNumberField.getText());
+        }
+    }
+
+    /**
+     * Implements JMeterGUIComponent.clearGui
+     */
+    @Override
+    public void clearGui() {
+        super.clearGui();
+        leftBoundaryField.setText(""); //$NON-NLS-1$        
+        rightBoundaryField.setText(""); //$NON-NLS-1$
+        defaultField.setText(""); //$NON-NLS-1$
+        refNameField.setText(""); //$NON-NLS-1$
+        emptyDefaultValue.setSelected(false);
+        matchNumberField.setText(""); //$NON-NLS-1$
+    }
+
+    private void init() { // WARNING: called from ctor so must not be overridden (i.e. must be private or final)
+        setLayout(new BorderLayout());
+        setBorder(makeBorder());
+
+        Box box = Box.createVerticalBox();
+        box.add(makeTitlePanel());
+        box.add(createScopePanel(true));
+        box.add(makeSourcePanel());
+        add(box, BorderLayout.NORTH);
+        add(makeParameterPanel(), BorderLayout.CENTER);
+    }
+
+    private JPanel makeSourcePanel() {
+        JPanel panel = new JPanel();
+        panel.setBorder(BorderFactory.createTitledBorder(JMeterUtils.getResString("regex_source"))); //$NON-NLS-1$
+
+        useBody = new JRadioButton(JMeterUtils.getResString("regex_src_body")); //$NON-NLS-1$
+        useUnescapedBody = new JRadioButton(JMeterUtils.getResString("regex_src_body_unescaped")); //$NON-NLS-1$
+        useBodyAsDocument = new JRadioButton(JMeterUtils.getResString("regex_src_body_as_document")); //$NON-NLS-1$
+        useHeaders = new JRadioButton(JMeterUtils.getResString("regex_src_hdrs")); //$NON-NLS-1$
+        useRequestHeaders = new JRadioButton(JMeterUtils.getResString("regex_src_hdrs_req")); //$NON-NLS-1$
+        useURL = new JRadioButton(JMeterUtils.getResString("regex_src_url")); //$NON-NLS-1$
+        useCode = new JRadioButton(JMeterUtils.getResString("assertion_code_resp")); //$NON-NLS-1$
+        useMessage = new JRadioButton(JMeterUtils.getResString("assertion_message_resp")); //$NON-NLS-1$
+
+        group = new ButtonGroup();
+        group.add(useBody);
+        group.add(useUnescapedBody);
+        group.add(useBodyAsDocument);
+        group.add(useHeaders);
+        group.add(useRequestHeaders);
+        group.add(useURL);
+        group.add(useCode);
+        group.add(useMessage);
+
+        panel.add(useBody);
+        panel.add(useUnescapedBody);
+        panel.add(useBodyAsDocument);
+        panel.add(useHeaders);
+        panel.add(useRequestHeaders);
+        panel.add(useURL);
+        panel.add(useCode);
+        panel.add(useMessage);
+
+        useBody.setSelected(true);
+
+        // So we know which button is selected
+        useBody.setActionCommand(RegexExtractor.USE_BODY);
+        useUnescapedBody.setActionCommand(RegexExtractor.USE_BODY_UNESCAPED);
+        useBodyAsDocument.setActionCommand(RegexExtractor.USE_BODY_AS_DOCUMENT);
+        useHeaders.setActionCommand(RegexExtractor.USE_HDRS);
+        useRequestHeaders.setActionCommand(RegexExtractor.USE_REQUEST_HDRS);
+        useURL.setActionCommand(RegexExtractor.USE_URL);
+        useCode.setActionCommand(RegexExtractor.USE_CODE);
+        useMessage.setActionCommand(RegexExtractor.USE_MESSAGE);
+
+        return panel;
+    }
+    
+    private JPanel makeParameterPanel() {        
+        leftBoundaryField = new JLabeledTextField(JMeterUtils.getResString("boundaryextractor_leftboundary_field")); //$NON-NLS-1$
+        rightBoundaryField = new JLabeledTextField(JMeterUtils.getResString("boundaryextractor_rightboundary_field")); //$NON-NLS-1$
+        refNameField = new JLabeledTextField(JMeterUtils.getResString("ref_name_field")); //$NON-NLS-1$
+        matchNumberField = new JLabeledTextField(JMeterUtils.getResString("match_num_field")); //$NON-NLS-1$
+
+        JPanel panel = new JPanel(new GridBagLayout());
+        GridBagConstraints gbc = new GridBagConstraints();
+        initConstraints(gbc);
+        addField(panel, refNameField, gbc);
+        resetContraints(gbc);
+        addField(panel, leftBoundaryField, gbc);
+        resetContraints(gbc);
+        addField(panel, rightBoundaryField, gbc);
+        resetContraints(gbc);
+        addField(panel, matchNumberField, gbc);
+        resetContraints(gbc);
+        gbc.weighty = 1;
+        
+        defaultField = new JLabeledTextField(JMeterUtils.getResString("default_value_field")); //$NON-NLS-1$
+        List<JComponent> item = defaultField.getComponentList();
+        panel.add(item.get(0), gbc.clone());
+        JPanel p = new JPanel(new BorderLayout());
+        p.add(item.get(1), BorderLayout.WEST);
+        emptyDefaultValue = new JCheckBox(JMeterUtils.getResString("boundaryextractor_empty_default_value"));
+        emptyDefaultValue.addItemListener(evt -> {
+            if(emptyDefaultValue.isSelected()) {
+                defaultField.setText("");
+            }
+            defaultField.setEnabled(!emptyDefaultValue.isSelected());
+        });
+        p.add(emptyDefaultValue, BorderLayout.CENTER);
+        gbc.gridx++;
+        gbc.weightx = 1;
+        gbc.fill = GridBagConstraints.HORIZONTAL;
+        panel.add(p, gbc.clone());
+        
+        return panel;
+    }
+
+    private void addField(JPanel panel, JLabeledTextField field, GridBagConstraints gbc) {
+        List<JComponent> item = field.getComponentList();
+        panel.add(item.get(0), gbc.clone());
+        gbc.gridx++;
+        gbc.weightx = 1;
+        gbc.fill=GridBagConstraints.HORIZONTAL;
+        panel.add(item.get(1), gbc.clone());
+    }
+
+    // Next line
+    private void resetContraints(GridBagConstraints gbc) {
+        gbc.gridx = 0;
+        gbc.gridy++;
+        gbc.weightx = 0;
+        gbc.fill=GridBagConstraints.NONE;
+    }
+
+    private void initConstraints(GridBagConstraints gbc) {
+        gbc.anchor = GridBagConstraints.NORTHWEST;
+        gbc.fill = GridBagConstraints.NONE;
+        gbc.gridheight = 1;
+        gbc.gridwidth = 1;
+        gbc.gridx = 0;
+        gbc.gridy = 0;
+        gbc.weightx = 0;
+        gbc.weighty = 0;
+    }
+}

Propchange: jmeter/trunk/src/components/org/apache/jmeter/extractor/gui/BoundaryExtractorGui.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/src/components/org/apache/jmeter/extractor/gui/BoundaryExtractorGui.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: jmeter/trunk/test/src/org/apache/jmeter/extractor/TestBoundaryExtractor.java
URL: http://svn.apache.org/viewvc/jmeter/trunk/test/src/org/apache/jmeter/extractor/TestBoundaryExtractor.java?rev=1812568&view=auto
==============================================================================
--- jmeter/trunk/test/src/org/apache/jmeter/extractor/TestBoundaryExtractor.java (added)
+++ jmeter/trunk/test/src/org/apache/jmeter/extractor/TestBoundaryExtractor.java Wed Oct 18 20:11:09 2017
@@ -0,0 +1,168 @@
+/*
+ * 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 static org.junit.Assert.assertThat;
+
+import org.apache.jmeter.samplers.SampleResult;
+import org.apache.jmeter.threads.JMeterContext;
+import org.apache.jmeter.threads.JMeterContextService;
+import org.apache.jmeter.threads.JMeterVariables;
+import org.hamcrest.CoreMatchers;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestBoundaryExtractor {
+
+    private static final String VAR_NAME = "varName";
+    private BoundaryExtractor extractor;
+    
+    private SampleResult result;
+
+    private JMeterVariables vars;
+
+    private JMeterContext context;
+
+    @Before
+    public void setUp() {
+        context = JMeterContextService.getContext();
+        extractor = new BoundaryExtractor();
+        extractor.setThreadContext(context);// This would be done by the run
+                                            // command
+        extractor.setRefName("regVal");
+        result = new SampleResult();
+        String data = "zazzd azd azd azd <t>value</t>azdazd azd azd";
+        result.setResponseData(data, null);
+        result.setResponseHeaders("Header1: Value1\nHeader2: Value2");
+        result.setResponseCode("abcd");
+        result.setResponseMessage("The quick brown fox");
+        vars = new JMeterVariables();
+        context.setVariables(vars);
+        context.setPreviousResult(result);
+    }
+    
+    @Test
+    public void testProcessAllElementsOneMatch() {
+        BoundaryExtractor processor = setupProcessor(context, "-1");
+        JMeterVariables vars = new JMeterVariables();
+        processor.setDefaultValue("NONE");
+        processor.setLeftBoundary("<t>");
+        processor.setRightBoundary("</t>");
+        processor.setRefName("varname");
+        processor.setScopeVariable("contentvar");
+        context.setVariables(vars);
+        vars.put("contentvar", "zazzd azd azd azd <t>one</t>azdazd azd azd");
+        processor.process();
+        assertThat(vars.get("varname"), CoreMatchers.is("NONE"));
+        assertThat(vars.get("varname_1"), CoreMatchers.is("one"));
+        assertThat(vars.get("varname_matchNr"), CoreMatchers.is("1"));
+    }
+
+    @Test
+    public void testProcessAllElementsMultipleMatches() {
+        BoundaryExtractor processor = setupProcessor(context, "-1");
+        JMeterVariables vars = new JMeterVariables();
+        processor.setDefaultValue("NONE");
+        processor.setLeftBoundary("<t>");
+        processor.setRightBoundary("</t>");
+        processor.setRefName("varname");
+        processor.setScopeVariable("contentvar");
+        context.setVariables(vars);
+        vars.put("contentvar", "zazzd azd azd azd \r\n<t>one</t>azdazd \r\nazd <t>two</t>azd");
+        processor.process();
+        assertThat(vars.get("varname_1"), CoreMatchers.is("one"));
+        assertThat(vars.get("varname_2"), CoreMatchers.is("two"));
+        assertThat(vars.get("varname_matchNr"), CoreMatchers.is("2"));
+    }
+
+    @Test
+    public void testProcessRandomElementMultipleMatches() {
+        BoundaryExtractor processor = setupProcessor(context, "0");
+        JMeterVariables vars = new JMeterVariables();
+        processor.setDefaultValue("NONE");
+        processor.setLeftBoundary("<t>");
+        processor.setRightBoundary("</t>");
+        processor.setRefName("varname");
+        processor.setScopeVariable("contentvar");
+        context.setVariables(vars);
+        vars.put("contentvar", "zazzd azd azd azd \r\n<t>one</t>azdazd \r\nazd <t>two</t>azd");
+        processor.process();
+        assertThat(vars.get("varname"), 
+                CoreMatchers.is(CoreMatchers.anyOf(CoreMatchers.is("one"), CoreMatchers.is("two"))));
+        assertThat(vars.get("varname_1"), CoreMatchers.is(CoreMatchers.nullValue()));
+        assertThat(vars.get("varname_2"), CoreMatchers.is(CoreMatchers.nullValue()));
+        assertThat(vars.get("varname_matchNr"), CoreMatchers.is(CoreMatchers.nullValue()));
+    }
+
+    @Test
+    public void testCaseEmptyResponse() {
+        BoundaryExtractor processor = setupProcessor(context, "-1");
+        JMeterVariables vars = new JMeterVariables();
+        processor.setDefaultValue("NONE");
+        processor.setLeftBoundary("<t>");
+        processor.setRightBoundary("</t>");
+        processor.setRefName("varname");
+        processor.setScopeVariable("contentvar");
+        context.setVariables(vars);
+        vars.put("contentvar", "zazzd azd azd azd \r\n<t>one</t>azdazd \r\nazd <t>two</t>azd");
+        processor.process();
+        assertThat(vars.get("varname_1"), CoreMatchers.is("one"));
+        assertThat(vars.get("varname_2"), CoreMatchers.is("two"));
+        assertThat(vars.get("varname_matchNr"), CoreMatchers.is("2"));
+        vars.put("contentvar", "");
+        processor.process();
+        assertThat(vars.get("varname_matchNr"), CoreMatchers.is("0"));
+        assertThat(vars.get("varname_1"), CoreMatchers.is(CoreMatchers.nullValue()));
+        assertThat(vars.get("varname_2"), CoreMatchers.is(CoreMatchers.nullValue()));
+    }
+
+    @Test
+    public void testCaseMatchOneWithZero() {
+        BoundaryExtractor processor = setupProcessor(context, "-1");
+        JMeterVariables vars = new JMeterVariables();
+        processor.setDefaultValue("NONE");
+        processor.setLeftBoundary("<t>");
+        processor.setRightBoundary("</t>");
+        processor.setRefName("varname");
+        processor.setScopeVariable("contentvar");
+        context.setVariables(vars);
+        vars.put("contentvar", "zazzd azd azd azd \r\n<t>one</t>azdazd \r\nazd <t>two</t>azd");
+        processor.process();
+        assertThat(vars.get("varname_1"), CoreMatchers.is("one"));
+        assertThat(vars.get("varname_2"), CoreMatchers.is("two"));
+        assertThat(vars.get("varname_matchNr"), CoreMatchers.is("2"));
+        vars.put("contentvar", "zaddazddad bvefvdv azd azvddfvfvd \r\n<t>A</t>azdazd \r\nfvdfv <t>B</t>azd");
+        processor.setMatchNumber("0");
+        processor.process();
+        assertThat(vars.get("varname"), CoreMatchers.is(CoreMatchers.anyOf(CoreMatchers.is("A"), CoreMatchers.is("B"))));
+        assertThat(vars.get("varname_matchNr"), CoreMatchers.is(CoreMatchers.nullValue()));
+        assertThat(vars.get("varname_1"), CoreMatchers.is(CoreMatchers.nullValue()));
+        assertThat(vars.get("varname_2"), CoreMatchers.is(CoreMatchers.nullValue()));
+    }
+
+    private BoundaryExtractor setupProcessor(JMeterContext context,
+            String matchNumber) {
+        BoundaryExtractor processor = new BoundaryExtractor();
+        processor.setThreadContext(context);
+        processor.setRefName(VAR_NAME);
+        processor.setMatchNumber(matchNumber);
+        return processor;
+    }
+
+}

Propchange: jmeter/trunk/test/src/org/apache/jmeter/extractor/TestBoundaryExtractor.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: jmeter/trunk/test/src/org/apache/jmeter/extractor/TestBoundaryExtractor.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: jmeter/trunk/xdocs/changes.xml
URL: http://svn.apache.org/viewvc/jmeter/trunk/xdocs/changes.xml?rev=1812568&r1=1812567&r2=1812568&view=diff
==============================================================================
--- jmeter/trunk/xdocs/changes.xml [utf-8] (original)
+++ jmeter/trunk/xdocs/changes.xml [utf-8] Wed Oct 18 20:11:09 2017
@@ -97,6 +97,7 @@ Summary
 
 <h3>Timers, Assertions, Config, Pre- &amp; Post-Processors</h3>
 <ul>
+    <li><bug>60213</bug>Boundary based extractor</li>
 </ul>
 
 <h3>Functions</h3>

Modified: jmeter/trunk/xdocs/usermanual/component_reference.xml
URL: http://svn.apache.org/viewvc/jmeter/trunk/xdocs/usermanual/component_reference.xml?rev=1812568&r1=1812567&r2=1812568&view=diff
==============================================================================
--- jmeter/trunk/xdocs/usermanual/component_reference.xml (original)
+++ jmeter/trunk/xdocs/usermanual/component_reference.xml Wed Oct 18 20:11:09 2017
@@ -6153,6 +6153,95 @@ It will allow you to extract in a very e
 <figure width="855" height="276" image="json-post-processor.png">JSON PostProcessor</figure>
 </component>
 
+<component name="Boundary Extractor" index="&sect-num;.8.10"  width="1127" height="277" screenshot="boundary_extractor.png">
+<description><p>Allows the user to extract values from a server response using left and right boundaries.  As a post-processor,
+this element will execute after each Sample request in its scope, testing the boundaries, extracting the requested values,
+generate the template string, and store the result into the given variable name.</p></description>
+<properties>
+        <property name="Name" required="">Descriptive name for this element that is shown in the tree.</property>
+        <property name="Apply to:" required="Yes">
+        This is for use with samplers that can generate sub-samples, 
+        e.g. HTTP Sampler with embedded resources, Mail Reader or samples generated by the Transaction Controller.
+        <ul>
+        <li><code>Main sample only</code> - only applies to the main sample</li>
+        <li><code>Sub-samples only</code> - only applies to the sub-samples</li>
+        <li><code>Main sample and sub-samples</code> - applies to both.</li>
+        <li><code>JMeter Variable</code> - assertion is to be applied to the contents of the named variable</li>
+        </ul>
+        Matching is applied to all qualifying samples in turn.
+        For example if there is a main sample and 3 sub-samples, each of which contains a single match test,
+        (i.e. 4 matches in total).
+        For match number = <code>3</code>, Sub-samples only, the extractor will match the 3<sup>rd</sup> sub-sample.
+        For match number = <code>3</code>, Main sample and sub-samples, the extractor will match the 2<sup>nd</sup> sub-sample (1<sup>st</sup> match is main sample).
+        For match number = <code>0</code> or negative, all qualifying samples will be processed.
+        For match number > <code>0</code>, matching will stop as soon as enough matches have been found.
+        </property>
+        <property name="Field to check" required="Yes">
+        The following fields can be checked:
+        <ul>
+        <li><code>Body</code> - the body of the response, e.g. the content of a web-page (excluding headers)</li>
+        <li><code>Body (unescaped)</code> - the body of the response, with all Html escape codes replaced.
+        Note that Html escapes are processed without regard to context, so some incorrect substitutions
+        may be made.
+        <note>Note that this option highly impacts performances, so use it only when absolutely necessary and be aware of its impacts</note>
+        </li>
+        <li><code>Body as a Document</code> - the extract text from various type of documents via Apache Tika (see <complink name="View Results Tree"/> Document view section).
+        <note>Note that the Body as a Document option can impact performances, so ensure it is OK for your test</note>
+        </li>
+        <li><code>Request Headers</code> - may not be present for non-HTTP samples</li>
+        <li><code>Response Headers</code> - may not be present for non-HTTP samples</li>
+        <li><code>URL</code></li>
+        <li><code>Response Code</code> - e.g. <code>200</code></li>
+        <li><code>Response Message</code> - e.g. <code>OK</code></li>
+        </ul>
+        Headers can be useful for HTTP samples; it may not be present for other sample types.
+        </property>
+        <property name="Reference Name" required="Yes">The name of the JMeter variable in which to store the result.  Also note that each group is stored as <code>[refname]_g#</code>, where <code>[refname]</code> is the string you entered as the reference name, and <code>#</code> is the group number, where group <code>0</code> is the entire match, group <code>1</code> is the match from the first set of parentheses, etc.</property>
+        <property name="Left Boundary" required="Yes">Left boundary of value to find</property>
+        <property name="Right Boundary" required="Yes">Left boundary of value to find</property>
+        <property name="Match No. (0 for Random)" required="Yes">Indicates which match to use.  The boundaries may match multiple times.  
+            <ul>
+                <li>Use a value of zero to indicate JMeter should choose a match at random.</li>
+                <li>A positive number N means to select the n<sup>th</sup> match.</li>
+                <li> Negative numbers are used in conjunction with the <complink name="ForEach Controller"/> - see below.</li>
+            </ul>
+        </property>
+        <property name="Default Value" required="No, but recommended">
+        If the boundaries do not match, then the reference variable will be set to the default value.
+        This is particularly useful for debugging tests. If no default is provided, then it is difficult to tell
+        whether the boundaries did not match, or maybe the wrong variable
+        is being used.
+        <p>
+        However, if you have several test elements that set the same variable, 
+        you may wish to leave the variable unchanged if the expression does not match.
+        In this case, remove the default value once debugging is complete.
+        </p> 
+        </property>
+</properties>
+<p>
+    If the match number is set to a non-negative number, and a match occurs, the variables are set as follows:
+</p>
+    <ul>
+        <li><code>refName</code> - the value of the extraction</li>
+    </ul>
+<p>
+    If no match occurs, then the <code>refName</code> variable is set to the default (unless this is absent). 
+</p>
+<p>
+    If the match number is set to a negative number, then all the possible matches in the sampler data are processed.
+    The variables are set as follows:
+</p>
+    <ul>
+        <li><code>refName_matchNr</code> - the number of matches found; could be <code>0</code></li>
+        <li><code>refName_<em>n</em></code>, where <code>n</code> = <code>1</code>, <code>2</code>, <code>3</code> etc. - the strings as generated by the template</li>
+        <li><code>refName_<em>n</em>_g<em>m</em></code>, where <code>m</code>=<code>0</code>, <code>1</code>, <code>2</code> - the groups for match <code>n</code></li>
+        <li><code>refName</code> - always set to the default value</li>
+    </ul>
+<p>
+    Note that the <code>refName</code> variable is always set to the default value in this case, 
+    and the associated group variables are not set.
+</p>
+</component>
 </section>
 
 <section name="&sect-num;.9 Miscellaneous Features" anchor="Miscellaneous_Features">