jmeter-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From pmoua...@apache.org
Subject [jmeter] branch master updated: New JMESPATH extractor (#489)
Date Sat, 14 Sep 2019 21:46:20 GMT
This is an automated email from the ASF dual-hosted git repository.

pmouawad pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jmeter.git


The following commit(s) were added to refs/heads/master by this push:
     new b15c922  New JMESPATH extractor (#489)
b15c922 is described below

commit b15c922eee225567a7ed7442ff8c6c848de421cc
Author: UBIK LOAD PACK <support@ubikloadpack.com>
AuthorDate: Sat Sep 14 23:46:15 2019 +0200

    New JMESPATH extractor (#489)
    
    JMES Path Extractor : new component that allows using JMESPATH (http://jmespath.org/)
as a new technology to extract data from JSON.
    
    This resolves Bug 63727 - New JMESPath Extractor to ease extraction from JSON
    https://bz.apache.org/bugzilla/show_bug.cgi?id=63727
    
    Contributed by UbikLoadPack Team
    https://ubikloadpack.com
---
 bin/saveservice.properties                         |   2 +
 checksum.properties                                |   2 +
 gradle.properties                                  |   2 +
 src/bom/build.gradle.kts                           |   2 +
 src/components/build.gradle.kts                    |   2 +
 .../extractor/json/jmespath/JMESPathExtractor.java | 251 +++++++++++++++++
 .../json/jmespath/gui/JMESPathExtractorGui.java    | 172 ++++++++++++
 .../json/jmespath/TestJMESPathExtractor.java       | 304 +++++++++++++++++++++
 .../java/org/apache/jmeter/save/SaveService.java   |   2 +-
 .../apache/jmeter/resources/messages.properties    |   2 +
 .../apache/jmeter/resources/messages_fr.properties |   2 +
 xdocs/changes.xml                                  |   1 +
 xdocs/usermanual/component_reference.xml           |  41 ++-
 13 files changed, 782 insertions(+), 3 deletions(-)

diff --git a/bin/saveservice.properties b/bin/saveservice.properties
index 0f768aa..d8f7a85 100644
--- a/bin/saveservice.properties
+++ b/bin/saveservice.properties
@@ -190,6 +190,8 @@ JDBCDataSource=org.apache.jmeter.protocol.jdbc.config.DataSourceElement
 JDBCPostProcessor=org.apache.jmeter.protocol.jdbc.processor.JDBCPostProcessor
 JDBCPreProcessor=org.apache.jmeter.protocol.jdbc.processor.JDBCPreProcessor
 JDBCSampler=org.apache.jmeter.protocol.jdbc.sampler.JDBCSampler
+JMESPathExtractor=org.apache.jmeter.extractor.json.jmespath.JMESPathExtractor
+JMESPathExtractorGui=org.apache.jmeter.extractor.json.jmespath.gui.JMESPathExtractorGui
 # Renamed to JMSSamplerGui; keep original entry for backwards compatibility
 JMSConfigGui=org.apache.jmeter.protocol.jms.control.gui.JMSConfigGui
 JMSProperties=org.apache.jmeter.protocol.jms.sampler.JMSProperties
diff --git a/checksum.properties b/checksum.properties
index 19b5703..8d73f41 100644
--- a/checksum.properties
+++ b/checksum.properties
@@ -70,6 +70,8 @@ dnsjava/dnsjava/2.1.8=A4BCB8BBB43906F42FAF1802C504CCC9C616E49AFD5DD7DB77676D13AA
 gradle.plugin.com.github.spotbugs/spotbugs-gradle-plugin/1.6.10=E7486B32EF6C9C14FE879814DA5F06CA6ECABF47195063A93E6FC8CD10119244C5A7BC3C71A4760CCE3AFFA9E9736336D345D8ED84EB65153C15683FA6529D92
 gradle.plugin.org.jetbrains.gradle.plugin.idea-ext/gradle-idea-ext/0.5=4A6B7FA6CD8C6FA82A517C396510E408F1C6FAB5FF6D4C68008F80718F05E5943755AA240F329C95661CCB0231114DD0F6D7C38EFBF73EE6B1ECC70850F40F7E
 info.picocli/picocli/3.9.6=3DDB7ABE7AFD0AFF754BE5616481C589E1354692BE583179503DDFD5F542A7F520960A6FB0D5320E171AE02E3A01B9E6B51E298B74518F8BF690EA9E9C7C8162
+io.burt/jmespath-core/0.3.0=86D1AAA40CB33172E7E3302A1031BCE34DB3B321101FFB268BF63A7FDC160E50E2F9B1AE2001D45D1904BEB941DDA74771DE1177B18BBC34146C0B5D6616D52E
+io.burt/jmespath-jackson/0.3.0=2CCE30F10B393431B4E316CD6467983AFD0970332C25A27F38CCBA7D79BA30BD8F2567C8C35945D40D95D9C7D1C8B3EA22EA3DC81E8442E666CD6629D0847640
 io.codearte.gradle.nexus/gradle-nexus-staging-plugin/0.20.0=E59970ADFA3343654A7A879299BADA0915D89F3A2FCCA53AACD26EEFE621D7EC598019CC32E9C38DDD35AC1B2E40B5167801A34E39E00FE72755D5FBB16236DD
 javax.activation/javax.activation-api/1.2.0=8EE0DB43AE402F0079A836EF2BFF5D15160E3FF9D585C3283F4CF474BE4EDD2FCC8714D8F032EFD72CAE77EC5F6D304FC24FA094D9CDBA5CF72966CC964AF6C9
 javax.mail/mail/1.5.0-b01=801A910F70DD743982872DCDEA46C24C6378E82C2CD2D970902A9CE5864191D3847BCFF6D5B81AEB89BABF056A30A70A03AA5687586D52CBFAAEAE3A5D6649F9
diff --git a/gradle.properties b/gradle.properties
index 5deb1aa..a3b0c49 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -71,6 +71,8 @@ jackson-databind.version=2.9.9.3
 javax.activation.version=1.2.0
 jcharts.version=0.7.5
 jdom.version=1.1.3
+jmespath-core.version=0.3.0
+jmespath-jackson.version=0.3.0
 jodd.version=5.0.6
 json-path.version=2.4.0
 json-smart.version=2.3
diff --git a/src/bom/build.gradle.kts b/src/bom/build.gradle.kts
index 1222682..5247b29 100644
--- a/src/bom/build.gradle.kts
+++ b/src/bom/build.gradle.kts
@@ -77,6 +77,8 @@ dependencies {
         apiv("commons-lang:commons-lang")
         apiv("commons-net:commons-net")
         apiv("dnsjava:dnsjava")
+        apiv("io.burt:jmespath-core")
+        apiv("io.burt:jmespath-jackson")
         apiv("javax.activation:javax.activation-api", "javax.activation")
         apiv("javax.mail:mail")
         apiv("jcharts:jcharts")
diff --git a/src/components/build.gradle.kts b/src/components/build.gradle.kts
index e58038c..5f00902 100644
--- a/src/components/build.gradle.kts
+++ b/src/components/build.gradle.kts
@@ -38,6 +38,8 @@ dependencies {
     compileOnly("javax.activation:javax.activation-api")
 
     implementation("com.github.ben-manes.caffeine:caffeine")
+    implementation("io.burt:jmespath-core")
+    implementation("io.burt:jmespath-jackson")
     implementation("jcharts:jcharts")
     implementation("oro:oro")
     implementation("net.minidev:json-smart")
diff --git a/src/components/src/main/java/org/apache/jmeter/extractor/json/jmespath/JMESPathExtractor.java
b/src/components/src/main/java/org/apache/jmeter/extractor/json/jmespath/JMESPathExtractor.java
new file mode 100644
index 0000000..aa61c03
--- /dev/null
+++ b/src/components/src/main/java/org/apache/jmeter/extractor/json/jmespath/JMESPathExtractor.java
@@ -0,0 +1,251 @@
+/*
+ * 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.json.jmespath;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.ArrayList;
+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.ThreadListener;
+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;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.github.benmanes.caffeine.cache.CacheLoader;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.LoadingCache;
+
+import io.burt.jmespath.Expression;
+import io.burt.jmespath.JmesPath;
+import io.burt.jmespath.RuntimeConfiguration;
+import io.burt.jmespath.function.FunctionRegistry;
+import io.burt.jmespath.jackson.JacksonRuntime;
+
+/**
+ * JMESPATH based extractor
+ *
+ * @since 5.2
+ */
+public class JMESPathExtractor extends AbstractScopedTestElement
+        implements Serializable, PostProcessor, ThreadListener {
+
+    private static final long serialVersionUID = 3849270294526207081L;
+
+    private static final Logger log = LoggerFactory.getLogger(JMESPathExtractor.class);
+    private static final String JMES_PATH_EXPRESSION = "JMESExtractor.jmesPathExpr"; // $NON-NLS-1$
+    private static final String REFERENCE_NAME = "JMESExtractor.referenceName"; // $NON-NLS-1$
+    private static final String DEFAULT_VALUE = "JMESExtractor.defaultValue"; // $NON-NLS-1$
+    private static final String MATCH_NUMBER = "JMESExtractor.matchNumber"; // $NON-NLS-1$
+    private static final String REF_MATCH_NR = "_matchNr"; // $NON-NLS-1$
+    private static final LoadingCache<String, Expression<JsonNode>> JMES_EXTRACTOR_CACHE
= Caffeine.newBuilder()
+            .maximumSize(JMeterUtils.getPropDefault("jmesextractor.parser.cache.size", 400))
+            .build(new JMESCacheLoader());
+
+    private static final class JMESCacheLoader implements CacheLoader<String, Expression<JsonNode>>
{
+        final JmesPath<JsonNode> runtime;
+
+        public JMESCacheLoader() {
+            runtime = new JacksonRuntime(new RuntimeConfiguration.Builder()
+                    .withFunctionRegistry(FunctionRegistry.defaultRegistry()).build());
+        }
+
+        @Override
+        public Expression<JsonNode> load(String jmesPathExpression) throws Exception
{
+            return runtime.compile(jmesPathExpression);
+        }
+    }
+
+    @Override
+    public void process() {
+        JMeterContext context = getThreadContext();
+        JMeterVariables vars = context.getVariables();
+        String jsonResponse = getData(vars, context);
+        String refName = getRefName();
+        String defaultValue = getDefaultValue();
+        int matchNumber = Integer.parseInt(getMatchNumber());
+        final String jsonPathExpression = getJmesPathExpression().trim();
+        clearOldRefVars(vars, refName);
+        if (StringUtils.isEmpty(jsonResponse)) {
+            if (log.isDebugEnabled()) {
+                log.debug("Response or source variable is null or empty for {}", getName());
+            }
+            vars.put(refName, defaultValue);
+        } else {
+            try {
+                JsonNode result = null;
+                ObjectMapper mapper = new ObjectMapper();
+                JsonNode actualObj = mapper.readValue(jsonResponse, JsonNode.class);
+                result = JMES_EXTRACTOR_CACHE.get(jsonPathExpression).search(actualObj);
+                if (result.isNull()) {
+                    vars.put(refName, defaultValue);
+                    vars.put(refName + REF_MATCH_NR, "0"); //$NON-NLS-1$
+                    if (matchNumber < 0) {
+                        log.debug("No value extracted, storing empty in: {}", refName);
+                    }
+                } else {
+                    List<String> resultList = splitJson(result);
+                    // if more than one value extracted, suffix with "_index"
+                    if (resultList.size() > 1) {
+                        if (matchNumber < 0) {
+                            // Extract all
+                            int index = 1;
+                            for (String extractedString : resultList) {
+                                vars.put(refName + "_" + index, extractedString); // $NON-NLS-1$
+                                index++;
+                            }
+                        } else if (matchNumber == 0) {
+                            // Random extraction
+                            int matchSize = resultList.size();
+                            int matchNr = JMeterUtils.getRandomInt(matchSize);
+                            placeObjectIntoVars(vars, refName, resultList, matchNr);
+                        } else {
+                            // extract at position
+                            if (matchNumber > resultList.size()) {
+                                if (log.isDebugEnabled()) {
+                                    log.debug(
+                                            "matchNumber({}) exceeds number of items found({}),
default value will be used",
+                                            matchNumber, resultList.size());
+                                }
+                                vars.put(refName, defaultValue);
+                            } else {
+                                placeObjectIntoVars(vars, refName, resultList, matchNumber
- 1);
+                            }
+                        }
+                    } else {
+                        // else just one value extracted
+                        String suffix = (matchNumber < 0) ? "_1" : "";
+                        placeObjectIntoVars(vars, refName + suffix, resultList, 0);
+                    }
+                    vars.put(refName + REF_MATCH_NR, Integer.toString(resultList.size()));
+                }
+            } catch (Exception e) {
+                // if something wrong, default value added
+                if (log.isDebugEnabled()) {
+                    log.debug("Error processing JSON content in {}, message: {}", getName(),
e.getLocalizedMessage(), e);
+                } else {
+                    log.debug("Error processing JSON content in {}, message: {}", getName(),
e.getLocalizedMessage());
+                }
+                vars.put(refName, defaultValue);
+            }
+        }
+    }
+
+    private String getData(JMeterVariables vars, JMeterContext context) {
+        String jsonResponse = null;
+        if (isScopeVariable()) {
+            jsonResponse = vars.get(getVariableName());
+            if (log.isDebugEnabled()) {
+                log.debug("JMESExtractor is using variable: {}, which content is: {}", getVariableName(),
jsonResponse);
+            }
+        } else {
+            SampleResult previousResult = context.getPreviousResult();
+            if (previousResult != null) {
+                jsonResponse = previousResult.getResponseDataAsString();
+            }
+            if (log.isDebugEnabled()) {
+                log.debug("JMESExtractor {} working on Response: {}", getName(), jsonResponse);
+            }
+        }
+        return jsonResponse;
+    }
+
+    public List<String> splitJson(JsonNode jsonNode) throws IOException {
+        List<String> splittedJsonElements = new ArrayList<>();
+        ObjectMapper mapper = new ObjectMapper();
+        if (jsonNode.isArray()) {
+            for (JsonNode element : (ArrayNode) jsonNode) {
+                splittedJsonElements.add(writeJsonNode(mapper, element));
+            }
+        } else {
+            splittedJsonElements.add(writeJsonNode(mapper, jsonNode));
+        }
+        return splittedJsonElements;
+    }
+
+    private static String writeJsonNode(ObjectMapper mapper, JsonNode element) throws JsonProcessingException
{
+        if (element.isTextual()) {
+            return element.asText();
+        } else {
+            return mapper.writeValueAsString(element);
+        }
+    }
+
+    void clearOldRefVars(JMeterVariables vars, String refName) {
+        vars.remove(refName + REF_MATCH_NR);
+        for (int i = 1; vars.get(refName + "_" + i) != null; i++) {
+            vars.remove(refName + "_" + i);
+        }
+    }
+
+    private void placeObjectIntoVars(JMeterVariables vars, String refName, List<String>
extractedValues, int matchNr) {
+        vars.put(refName, extractedValues.get(matchNr));
+    }
+
+    public String getJmesPathExpression() {
+        return getPropertyAsString(JMES_PATH_EXPRESSION);
+    }
+
+    public void setJmesPathExpression(String jsonPath) {
+        setProperty(JMES_PATH_EXPRESSION, jsonPath);
+    }
+
+    public String getRefName() {
+        return getPropertyAsString(REFERENCE_NAME);
+    }
+
+    public void setRefName(String refName) {
+        setProperty(REFERENCE_NAME, refName);
+    }
+
+    public String getDefaultValue() {
+        return getPropertyAsString(DEFAULT_VALUE);
+    }
+
+    public void setDefaultValue(String defaultValue) {
+        setProperty(DEFAULT_VALUE, defaultValue, ""); // $NON-NLS-1$
+    }
+
+    @Override
+    public void threadStarted() {
+        // NOOP
+    }
+
+    @Override
+    public void threadFinished() {
+        JMES_EXTRACTOR_CACHE.cleanUp();
+    }
+
+    public void setMatchNumber(String matchNumber) {
+        setProperty(MATCH_NUMBER, matchNumber);
+    }
+
+    public String getMatchNumber() {
+        return getPropertyAsString(MATCH_NUMBER);
+    }
+}
diff --git a/src/components/src/main/java/org/apache/jmeter/extractor/json/jmespath/gui/JMESPathExtractorGui.java
b/src/components/src/main/java/org/apache/jmeter/extractor/json/jmespath/gui/JMESPathExtractorGui.java
new file mode 100644
index 0000000..0818d47
--- /dev/null
+++ b/src/components/src/main/java/org/apache/jmeter/extractor/json/jmespath/gui/JMESPathExtractorGui.java
@@ -0,0 +1,172 @@
+/*
+ * 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.json.jmespath.gui;
+
+import java.awt.BorderLayout;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.util.List;
+
+import javax.swing.Box;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+
+import org.apache.jmeter.extractor.json.jmespath.JMESPathExtractor;
+import org.apache.jmeter.gui.GUIMenuSortOrder;
+import org.apache.jmeter.processor.gui.AbstractPostProcessorGui;
+import org.apache.jmeter.testelement.TestElement;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jorphan.gui.JLabeledTextField;
+
+/**
+ * GUI for {@link JMESExtractor}
+ *
+ * @since 5.2
+ */
+@GUIMenuSortOrder(2)
+public class JMESPathExtractorGui extends AbstractPostProcessorGui {
+
+    private static final long serialVersionUID = -4825532539405119033L;
+    private JLabeledTextField defaultValueField;
+    private JLabeledTextField jmesPathExpressionField;
+    private JLabeledTextField refNameField;
+    private JLabeledTextField matchNumberField;
+
+    public JMESPathExtractorGui() {
+        super();
+        init();
+    }
+
+    @Override
+    public String getLabelResource() {
+        return "jmes_extractor_title";//$NON-NLS-1$
+    }
+
+    @Override
+    public void configure(TestElement element) {
+        super.configure(element);
+        JMESPathExtractor config = (JMESPathExtractor) element;
+        showScopeSettings(config, true);
+        refNameField.setText(config.getRefName());
+        jmesPathExpressionField.setText(config.getJmesPathExpression());
+        matchNumberField.setText(config.getMatchNumber());
+        defaultValueField.setText(config.getDefaultValue());
+    }
+
+    /**
+     * @see org.apache.jmeter.gui.JMeterGUIComponent#createTestElement()
+     */
+    @Override
+    public TestElement createTestElement() {
+        JMESPathExtractor config = new JMESPathExtractor();
+        modifyTestElement(config);
+        return config;
+    }
+
+    /**
+     * 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 c) {
+        super.configureTestElement(c);
+        if (c instanceof JMESPathExtractor) {
+            JMESPathExtractor config = (JMESPathExtractor) c;
+            saveScopeSettings(config);
+            config.setRefName(refNameField.getText());
+            config.setJmesPathExpression(jmesPathExpressionField.getText());
+            config.setDefaultValue(defaultValueField.getText());
+            config.setMatchNumber(matchNumberField.getText());
+        }
+    }
+
+    /**
+     * Implements JMeterGUIComponent.clearGui
+     */
+    @Override
+    public void clearGui() {
+        super.clearGui();
+        refNameField.setText(""); //$NON-NLS-1$
+        jmesPathExpressionField.setText(""); //$NON-NLS-1$
+        matchNumberField.setText(""); //$NON-NLS-1$
+        defaultValueField.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));
+        add(box, BorderLayout.NORTH);
+        add(makeParameterPanel(), BorderLayout.CENTER);
+
+    }
+
+    private JPanel makeParameterPanel() {
+        refNameField = new JLabeledTextField(JMeterUtils.getResString("jsonpp_variable_names"));//$NON-NLS-1$
+        jmesPathExpressionField = new JLabeledTextField(JMeterUtils.getResString("jmes_path_expressions"));//$NON-NLS-1$
+        matchNumberField = new JLabeledTextField(JMeterUtils.getResString("jsonpp_match_numbers"));//$NON-NLS-1$
+        defaultValueField = new JLabeledTextField(JMeterUtils.getResString("jsonpp_default_values"));//$NON-NLS-1$
+        JPanel panel = new JPanel(new GridBagLayout());
+        GridBagConstraints gbc = new GridBagConstraints();
+        initConstraints(gbc);
+        addField(panel, refNameField, gbc);
+        nextLine(gbc);
+        addField(panel, jmesPathExpressionField, gbc);
+        nextLine(gbc);
+        addField(panel, matchNumberField, gbc);
+        nextLine(gbc);
+        nextLine(gbc);
+        gbc.weighty = 1;
+        addField(panel, defaultValueField, gbc);
+        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());
+    }
+
+    private void nextLine(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;
+    }
+}
diff --git a/src/components/src/test/java/org/apache/jmeter/extractor/json/jmespath/TestJMESPathExtractor.java
b/src/components/src/test/java/org/apache/jmeter/extractor/json/jmespath/TestJMESPathExtractor.java
new file mode 100644
index 0000000..d74a8cf
--- /dev/null
+++ b/src/components/src/test/java/org/apache/jmeter/extractor/json/jmespath/TestJMESPathExtractor.java
@@ -0,0 +1,304 @@
+/*
+ * 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.json.jmespath;
+
+import static org.junit.Assert.assertThat;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+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.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Enclosed.class)
+public class TestJMESPathExtractor {
+    private static final String DEFAULT_VALUE = "NONE"; // $NON-NLS-1$
+    private static final String REFERENCE_NAME = "varname"; // $NON-NLS-1$
+    private static final String REFERENCE_NAME_MATCH_NUMBER = "varname_matchNr"; // $NON-NLS-1$
+
+    private static JMESPathExtractor setupProcessor(JMeterVariables vars, SampleResult sampleResult,
String data, boolean isSourceVars, String matchNumbers) {
+        JMeterContext jmctx = JMeterContextService.getContext();
+        jmctx.setVariables(vars);
+        jmctx.setPreviousResult(sampleResult);
+        JMESPathExtractor processor = new JMESPathExtractor();
+        processor.setThreadContext(jmctx);
+        processor.setRefName(REFERENCE_NAME);
+        processor.setMatchNumber(matchNumbers);
+        processor.setDefaultValue(DEFAULT_VALUE);
+        if (isSourceVars) {
+            vars.put("contentvar", data);
+            processor.setScopeVariable("contentvar");
+        } else {
+            sampleResult.setResponseData(data, null);
+            processor.setScopeAll();
+        }
+        return processor;
+    }
+
+    @RunWith(Parameterized.class)
+    public static class OneMatchOnAllExtractedValues {
+
+        @Parameters
+        public static Collection<String[]> data() {
+            return Arrays.asList(new String[][] {
+                {"[\"one\"]", "[*]", "one", "1"},
+                {"{\"a\": {\"b\": {\"c\": {\"d\": \"value\"}}}}", "a.b.c.d", "value", "1"},
+                {"{\r\n" + "  \"people\": [\r\n" + "    {\"first\": \"James\", \"last\":
\"d\"},\r\n"
+                        + "    {\"first\": \"Jacob\", \"last\": \"e\"},\r\n"
+                        + "    {\"first\": \"Jayden\", \"last\": \"f\"},\r\n" + "    {\"missing\":
\"different\"}\r\n"
+                        + "  ],\r\n" + "  \"foo\": {\"bar\": \"baz\"}\r\n" + "}", "people[2]",
+                        "{\"first\":\"Jayden\",\"last\":\"f\"}",
+                        "1"}
+            });
+        }
+
+        private String data;
+        private String jmesPath;
+        private String expectedResult;
+        private String expectedMatchNumber;
+
+        public OneMatchOnAllExtractedValues(String data, String jmesPath, String expectedResult,
String expectedMatchNumber) {
+            this.data = data;
+            this.jmesPath = jmesPath;
+            this.expectedResult = expectedResult;
+            this.expectedMatchNumber = expectedMatchNumber;
+        }
+
+        @Test
+        public void testFromVars() {
+            test(true);
+        }
+
+        @Test
+        public void testFromSampleResult() {
+            test(false);
+        }
+
+        public void test(boolean fromVars) {
+            JMeterVariables vars = new JMeterVariables();
+            SampleResult sampleResult = new SampleResult();
+            JMESPathExtractor processor = setupProcessor(vars, sampleResult, data, fromVars,
"-1");
+            processor.setJmesPathExpression(jmesPath);
+            processor.process();
+            assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(CoreMatchers.nullValue()));
+            assertThat(vars.get(REFERENCE_NAME + "_1"), CoreMatchers.is(expectedResult));
+            assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(expectedMatchNumber));
+
+            processor.clearOldRefVars(vars, REFERENCE_NAME);
+            assertThat(vars.get(REFERENCE_NAME + "_1"), CoreMatchers.is(CoreMatchers.nullValue()));
+            assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(CoreMatchers.nullValue()));
+        }
+    }
+
+    @RunWith(Parameterized.class)
+    public static class MultipleMatchesOnAllExtractedValues {
+
+        @Parameters
+        public static Collection<Object[]> data() {
+            return Arrays.asList(new Object[][] {
+                {"[\"one\", \"two\"]", "[*]", new String[] {"one", "two"}, "2"},
+                {"[\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"]", "[0:3]", new String[] {"a",
"b","c"}, "3"},
+                {"{\r\n" + "  \"people\": [\r\n" + "    {\"first\": \"James\", \"last\":
\"d\"},\r\n"
+                        + "    {\"first\": \"Jacob\", \"last\": \"e\"},\r\n"
+                        + "    {\"first\": \"Jayden\", \"last\": \"f\"},\r\n" + "    {\"missing\":
\"different\"}\r\n"
+                        + "  ],\r\n" + "  \"foo\": {\"bar\": \"baz\"}\r\n" + "}", "people[:2].first",
new String[] {"James", "Jacob"}, "2" },
+            });
+        }
+
+        private String data;
+        private String jmesPath;
+        private String[] expectedResults;
+        private String expectedMatchNumber;
+
+        public MultipleMatchesOnAllExtractedValues(String data, String jmesPath, String[]
expectedResults, String expectedMatchNumber) {
+            this.data = data;
+            this.jmesPath = jmesPath;
+            this.expectedResults = expectedResults;
+            this.expectedMatchNumber = expectedMatchNumber;
+        }
+
+        @Test
+        public void testFromVars() {
+            test(true);
+        }
+
+        @Test
+        public void testFromSampleResult() {
+            test(false);
+        }
+
+        public void test(boolean fromVars) {
+            SampleResult sampleResult = new SampleResult();
+            JMeterVariables vars = new JMeterVariables();
+            JMESPathExtractor processor = setupProcessor(vars, sampleResult, data, fromVars,
"-1");
+            // test1
+            processor.setJmesPathExpression(jmesPath);
+            processor.process();
+            assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(CoreMatchers.nullValue()));
+            for (int i = 0; i < expectedResults.length; i++) {
+                assertThat(vars.get(REFERENCE_NAME + "_"+(i+1)), CoreMatchers.is(expectedResults[i]));
+            }
+            assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(expectedMatchNumber));
+        }
+    }
+
+    @RunWith(Parameterized.class)
+    public static class MatchNumberMoreThanZeroOn1ExtractedValue {
+
+        private static final String TEST_DATA = "{\r\n" + "  \"people\": [\r\n" + "    {\"first\":
\"James\", \"last\": \"d\", \"age\":10},\r\n"
+                + "    {\"first\": \"Jacob\", \"last\": \"e\", \"age\":20},\r\n"
+                + "    {\"first\": \"Jayden\", \"last\": \"f\", \"age\":30},\r\n"
+                + "    {\"missing\": \"different\"}\r\n" + "  ],\r\n" + "  \"foo\": {\"bar\":
\"baz\"}\r\n"
+                + "}";
+
+        @Parameters
+        public static Collection<String[]> data() {
+            return Arrays.asList(new String[][] {
+                {TEST_DATA, "people[:3].first", "1", "James", "3"},
+                {TEST_DATA, "people[:3].first", "2", "Jacob", "3"},
+                {TEST_DATA, "people[:3].first", "3", "Jayden", "3"},
+                {TEST_DATA, "people[:3].age", "3", "30", "3"},
+                {TEST_DATA, "people[:3].first", "4", DEFAULT_VALUE, "3"}
+            });
+        }
+
+        private String data;
+        private String jmesPath;
+        private String expectedResult;
+        private String expectedMatchNumber;
+        private String matchNumber;
+
+        public MatchNumberMoreThanZeroOn1ExtractedValue(String data, String jmesPath,
+                String matchNumber, String expectedResult, String expectedMatchNumber) {
+            this.data = data;
+            this.jmesPath = jmesPath;
+            this.expectedResult = expectedResult;
+            this.matchNumber = matchNumber;
+            this.expectedMatchNumber = expectedMatchNumber;
+        }
+
+        @Test
+        public void testFromVars() {
+            test(true);
+        }
+
+        @Test
+        public void testFromSampleResult() {
+            test(false);
+        }
+
+        public void test(boolean fromVars) {
+            SampleResult sampleResult = new SampleResult();
+            JMeterVariables vars = new JMeterVariables();
+            JMESPathExtractor processor = setupProcessor(vars, sampleResult, data, fromVars,
"1");
+            processor.setMatchNumber(matchNumber);
+            processor.setJmesPathExpression(jmesPath);
+            processor.process();
+            assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(expectedResult));
+            assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(expectedMatchNumber));
+        }
+    }
+
+    @RunWith(Parameterized.class)
+    public static class SourceVarOrResponse {
+        private boolean fromVariables;
+
+        @Parameters
+        public static Collection<Boolean> data() {
+            return Arrays.asList(new Boolean[] {Boolean.TRUE, Boolean.FALSE} );
+        }
+
+        public SourceVarOrResponse(boolean fromVariables) {
+            this.fromVariables = fromVariables;
+        }
+
+        @Test
+        public void testRandomElementOneMatch() {
+            SampleResult sampleResult = new SampleResult();
+            JMeterVariables vars = new JMeterVariables();
+            JMESPathExtractor processor = setupProcessor(vars, sampleResult, "{\"a\": {\"b\":
{\"c\": {\"d\": \"value\"}}}}", fromVariables, "0");
+
+            processor.setJmesPathExpression("a.b.c.d");
+            processor.process();
+            assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is("value"));
+            assertThat(vars.get(REFERENCE_NAME + "_1"), CoreMatchers.is(CoreMatchers.nullValue()));
+            assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is("1"));
+        }
+
+        @Test
+        public void testRandomElementMultipleMatches() {
+            SampleResult sampleResult = new SampleResult();
+            JMeterVariables vars = new JMeterVariables();
+            JMESPathExtractor processor = setupProcessor(vars, sampleResult, "[\"one\", \"two\"]",
fromVariables, "0");
+
+            processor.setJmesPathExpression("[*]");
+            processor.process();
+            assertThat(vars.get(REFERENCE_NAME),
+                    CoreMatchers.is(CoreMatchers.anyOf(CoreMatchers.is("one"), CoreMatchers.is("two"))));
+            assertThat(vars.get(REFERENCE_NAME + "_1"), CoreMatchers.is(CoreMatchers.nullValue()));
+            assertThat(vars.get(REFERENCE_NAME + "_2"), CoreMatchers.is(CoreMatchers.nullValue()));
+            assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is("2"));
+        }
+
+        @Test
+        public void testEmptySourceData() {
+            SampleResult sampleResult = new SampleResult();
+            JMeterVariables vars = new JMeterVariables();
+            JMESPathExtractor processor = setupProcessor(vars, sampleResult, "", fromVariables,
"-1");
+
+            processor.setJmesPathExpression("[*]");
+            processor.process();
+            assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(DEFAULT_VALUE));
+            assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is(CoreMatchers.nullValue()));
+        }
+
+        @Test
+        public void testErrorInJMESPath() {
+            SampleResult sampleResult = new SampleResult();
+            JMeterVariables vars = new JMeterVariables();
+            JMESPathExtractor processor = setupProcessor(vars, sampleResult, "{\"a\": {\"b\":
{\"c\": {\"d\": \"value\"}}}}", fromVariables, "-1");
+
+            processor.setJmesPathExpression("$.k");
+            processor.process();
+            assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(DEFAULT_VALUE));
+            assertThat(vars.get(REFERENCE_NAME+ "_1"), CoreMatchers.nullValue());
+            assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.nullValue());
+        }
+
+        @Test
+        public void testNoMatch() {
+            SampleResult sampleResult = new SampleResult();
+            JMeterVariables vars = new JMeterVariables();
+            JMESPathExtractor processor = setupProcessor(vars, sampleResult, "{\"a\": {\"b\":
{\"c\": {\"d\": \"value\"}}}}", fromVariables, "-1");
+
+            processor.setJmesPathExpression("a.b.c.f");
+            processor.process();
+            assertThat(vars.get(REFERENCE_NAME), CoreMatchers.is(DEFAULT_VALUE));
+            assertThat(vars.get(REFERENCE_NAME+ "_1"), CoreMatchers.nullValue());
+            assertThat(vars.get(REFERENCE_NAME_MATCH_NUMBER), CoreMatchers.is("0"));
+        }
+    }
+}
diff --git a/src/core/src/main/java/org/apache/jmeter/save/SaveService.java b/src/core/src/main/java/org/apache/jmeter/save/SaveService.java
index dbe9841..50c1e41 100644
--- a/src/core/src/main/java/org/apache/jmeter/save/SaveService.java
+++ b/src/core/src/main/java/org/apache/jmeter/save/SaveService.java
@@ -156,7 +156,7 @@ public class SaveService {
     private static String fileVersion = ""; // computed from saveservice.properties file//
$NON-NLS-1$
     // Must match the sha1 checksum of the file saveservice.properties (without newline character),
     // used to ensure saveservice.properties and SaveService are updated simultaneously
-    static final String FILEVERSION = "890bb3bbf003d8f127c3eea786294b65a44f9b19"; // Expected
value $NON-NLS-1$
+    static final String FILEVERSION = "5639a75022428edf8ae98232d261d4d27d4950f1"; // Expected
value $NON-NLS-1$
 
     private static String fileEncoding = ""; // read from properties file// $NON-NLS-1$
 
diff --git a/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties b/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties
index 65aa2b4..4363000 100644
--- a/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties
+++ b/src/core/src/main/resources/org/apache/jmeter/resources/messages.properties
@@ -507,6 +507,8 @@ java_request_defaults=Java Request Defaults
 java_request_warning=<html>Classname not found in classpath, ensure you add the required
jar and restart. <br/> If you modify "Classname" before you may lose the parameters
of the original test plan.<html>
 javascript_expression=JavaScript expression to evaluate
 jexl_expression=JEXL expression to evaluate
+jmes_extractor_title=JMESPath Extractor
+jmes_path_expressions=JMESPath expressions\:
 jms_auth_required=Required
 jms_bytes_message=Bytes Message
 jms_client_caption=Receiver client uses MessageConsumer.receive() to listen for message.
diff --git a/src/core/src/main/resources/org/apache/jmeter/resources/messages_fr.properties
b/src/core/src/main/resources/org/apache/jmeter/resources/messages_fr.properties
index f4e6d65..a5ef5a1 100644
--- a/src/core/src/main/resources/org/apache/jmeter/resources/messages_fr.properties
+++ b/src/core/src/main/resources/org/apache/jmeter/resources/messages_fr.properties
@@ -501,6 +501,8 @@ java_request_defaults=Requête Java par défaut
 java_request_warning=<html>Classe introuvable dans le classpath, veuillez ajouter le
jar la contenant et redémarrer.<br/>Ne modifiez pas "Nom de classe" avant sinon vous
perdrez les paramètres.</html>
 javascript_expression=Expression JavaScript à évaluer
 jexl_expression=Expression JEXL à évaluer
+jmes_extractor_title=Extracteur JMESPath
+jmes_path_expressions=Expressions JMESPath\:
 jms_auth_required=Obligatoire
 jms_bytes_message=Message binaire
 jms_client_caption=Le client récepteur utilise MessageConsumer.receive () pour écouter
les messages.
diff --git a/xdocs/changes.xml b/xdocs/changes.xml
index a692cba..79c6091 100644
--- a/xdocs/changes.xml
+++ b/xdocs/changes.xml
@@ -109,6 +109,7 @@ to view the last release notes of version 5.1.1.
   <li><bug>62787</bug>New <code>XPath2 Assertion</code> supporting
XPath2 with better performances than <code>XPath Assertion</code>. Contributed
by Ubik Load Pack (support at ubikloadpack.com)</li>
   <li><bug>63643</bug>Skip BOM on files opened through <code>FileServer</code>
and use the BOM to detect the character encoding,
       if none is given explicitly. Reported by Havlicek Honza (havlicek.honza at gmail.com)</li>
+  <li><bug>63727</bug>New <code>JMESPath Extractor</code> element
to ease extraction from JSON. Contributed by Ubik Load Pack (support at ubikloadpack.com)</li>
 </ul>
 
 <h3>Functions</h3>
diff --git a/xdocs/usermanual/component_reference.xml b/xdocs/usermanual/component_reference.xml
index 6e80b50..44c8624 100644
--- a/xdocs/usermanual/component_reference.xml
+++ b/xdocs/usermanual/component_reference.xml
@@ -5986,7 +5986,44 @@ by:
 where "<code>uri-for-namespace</code>" is the uri for the "<code>mynamespace</code>"
namespace.(not applicable if Tidy is selected)
 </component>
 
-<component name="Result Status Action Handler" index="&sect-num;.8.4"  width="613"
height="133" screenshot="resultstatusactionhandler.png">
+<component name="JMESPath Extractor" index="&sect-num;.8.4"  width="729" height="317"
screenshot="jmes_extractor.png">
+    <description>This test element allows the user to extract value(s) from 
+        structured response - XML or (X)HTML - using JMESPath
+        query language.
+   </description>
+   <note>In the XPATH Extractor we support to extract multiple xpaths at the same time,
but in JMES Extractor only 
+   one JMES Expression can be entered at a time.
+   </note>
+   <properties>
+       <property name="Name" required="No">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 Name to use</code> - extraction is to
be applied to the contents of the named variable</li>
+        </ul>
+       </property>
+    <property name="Name of created variable" required="Yes">The name of the JMeter
variable in which to store the result.</property>
+    <property name="JMESPath expressions" required="Yes">Element query in JMESPath
query language. Can return the matched result.</property>
+    <property name="Match No. (0 for Random)" required="No">If the JMESPath query leads
to many results, you can choose which one(s) to extract as Variables:
+    <ul>
+        <li><code>0</code> : means random</li>
+        <li><code>-1</code> means extract all results (default value),
they will be named as <code><em>&lt;variable name&gt;</em>_N</code>
(where <code>N</code> goes from 1 to Number of results)</li>
+        <li><code>X</code> : means extract the X<sup>th</sup>
result. If this X<sup>th</sup> is greater than number of matches, then nothing
is returned. Default value will be used</li>
+    </ul>
+    </property>
+    <property name="Default Value" required="">Default value returned when no match
found. 
+    It is also returned if the node has no value and the fragment option is not selected.</property>
+   </properties>
+      <p>JMESPath is a query language for JSON. It is described in an ABNF grammar
with a complete specification. This ensures that the language syntax is precisely defined.

+    See <a href="http://jmespath.org/">JMESPath Reference</a> for more information.
Here are also some examples <a href="http://jmespath.org/tutorial.html">JMESPath Example</a>.
+   </p>
+</component>
+
+<component name="Result Status Action Handler" index="&sect-num;.8.5"  width="613"
height="133" screenshot="resultstatusactionhandler.png">
    <description>This test element allows the user to stop the thread or the whole test
if the relevant sampler failed.
    </description>
    <properties>
@@ -6005,7 +6042,7 @@ where "<code>uri-for-namespace</code>" is the uri for the
"<code>mynamespace</co
    </properties>
 </component>
 
-<component name="BeanShell PostProcessor"  index="&sect-num;.8.5"  width="847" height="633"
screenshot="beanshell_postprocessor.png">
+<component name="BeanShell PostProcessor"  index="&sect-num;.8.6"  width="847" height="633"
screenshot="beanshell_postprocessor.png">
 <description>
 <p>
 The BeanShell PreProcessor allows arbitrary code to be applied after taking a sample.


Mime
View raw message