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 2019/09/14 21:46:20 UTC
[jmeter] branch master updated: New JMESPATH extractor (#489)
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 <su...@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="§-num;.8.4" width="613" height="133" screenshot="resultstatusactionhandler.png">
+<component name="JMESPath Extractor" index="§-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><variable name></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="§-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="§-num;.8.5" width="847" height="633" screenshot="beanshell_postprocessor.png">
+<component name="BeanShell PostProcessor" index="§-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.