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 82e56be BZ 64752 - Add GraphQL/HTTP Request Sampler (#627)
82e56be is described below
commit 82e56beb04505af96d1e9ce327f67aa8eba99e06
Author: Woonsan Ko <woonsan@users.noreply.github.com>
AuthorDate: Sun Oct 4 05:06:56 2020 -0400
BZ 64752 - Add GraphQL/HTTP Request Sampler (#627)
* BZ-64752: adding GraphQL HTTP Sampler GUI components
* BZ-64752: javadocs
* BZ-64752: fixing tab selection problem; disable tab validation in graphql ui
* BZ-64752: (de)serializing test element with graphql query and vars
* BZ-64752: (de)serializing using gson
* BZ-64752: removing unnecessary GraphQLHTTPSampler
* BZ-64752: adding operationName input field
* BZ-64752: support GET method
* BZ-64752: init operationName from test elem
* BZ-64752: adding a simple graphql test plan demo
* BZ-64752: show advanced pane
* BZ-64752: add gson info to lib/aareadme.txt
* BZ-64752: screenshot and default constructor
* BZ-64752: documentation on GraphQLHTTPRequest
* BZ-64752: record in changes.xml
* BZ-64752: add gson.jar to expected_release_jars.csv
* BZ-64752: removing unnecessary, untranslated messages
* BZ-64752: utility for graphql param serialization and unit test
* BZ-64752: replace gson with jackson for graphql (de)serialization
* BZ-64752: remove gson jar from expected release jars
* BZ-64752: correcting French translation, thanks to pmouawad
* BZ-64752: graphql http recording support
* BZ-64752: checkbox option to switch on/off auto graphql req detection, true by default
* BZ-64752: precise json content type checking; encode in GET
* BZ-64752: French translation for graphql recording option, thanks to @ubikloadpack
Co-authored-by: Woonsan Ko <woonsan.ko@bloomreach.com>
---
bin/saveservice.properties | 1 +
.../java/org/apache/jmeter/save/SaveService.java | 2 +-
.../org/apache/jmeter/gui/util/textarea.properties | 1 +
.../apache/jmeter/resources/messages.properties | 7 +
.../apache/jmeter/resources/messages_fr.properties | 7 +
src/protocol/build.gradle.kts | 2 +
.../protocol/http/config/GraphQLRequestParams.java | 67 +++++
.../config/gui/AbstractValidationTabbedPane.java | 86 +++++++
.../http/config/gui/GraphQLUrlConfigGui.java | 190 ++++++++++++++
.../http/config/gui/UrlConfigDefaults.java | 272 +++++++++++++++++++++
.../protocol/http/config/gui/UrlConfigGui.java | 129 +++++-----
.../protocol/http/control/HeaderManager.java | 20 ++
.../http/control/gui/GraphQLHTTPSamplerGui.java | 75 ++++++
.../http/control/gui/HttpTestSampleGui.java | 62 +++--
.../protocol/http/proxy/DefaultSamplerCreator.java | 44 ++++
.../jmeter/protocol/http/proxy/HttpRequestHdr.java | 18 ++
.../apache/jmeter/protocol/http/proxy/Proxy.java | 2 +
.../jmeter/protocol/http/proxy/ProxyControl.java | 12 +
.../protocol/http/proxy/gui/ProxyControlGui.java | 22 ++
.../http/util/GraphQLRequestParamUtils.java | 254 +++++++++++++++++++
.../sampler/GraphQLHTTPSamplerResources.properties | 27 ++
.../http/util/TestGraphQLRequestParamUtils.java | 212 ++++++++++++++++
xdocs/changes.xml | 1 +
xdocs/demos/SimpleGraphQLTestPlan.jmx | 135 ++++++++++
.../screenshots/graphql-http-request-vars.png | Bin 0 -> 110286 bytes
xdocs/images/screenshots/graphql-http-request.png | Bin 0 -> 115587 bytes
xdocs/usermanual/component_reference.xml | 28 ++-
27 files changed, 1596 insertions(+), 80 deletions(-)
diff --git a/bin/saveservice.properties b/bin/saveservice.properties
index 6f0b44b..b2ec167 100644
--- a/bin/saveservice.properties
+++ b/bin/saveservice.properties
@@ -150,6 +150,7 @@ GaussianRandomTimerGui=org.apache.jmeter.timers.gui.GaussianRandomTimerGui
GenericController=org.apache.jmeter.control.GenericController
GraphAccumVisualizer=org.apache.jmeter.visualizers.GraphAccumVisualizer
GraphVisualizer=org.apache.jmeter.visualizers.GraphVisualizer
+GraphQLHTTPSamplerGui=org.apache.jmeter.protocol.http.control.gui.GraphQLHTTPSamplerGui
Header=org.apache.jmeter.protocol.http.control.Header
HeaderManager=org.apache.jmeter.protocol.http.control.HeaderManager
HeaderPanel=org.apache.jmeter.protocol.http.gui.HeaderPanel
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 0d7cf07..2955c38 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
@@ -155,7 +155,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 = "56ae8319b2b02d33eb1028c4460db770cf246b5c"; // Expected value $NON-NLS-1$
+ static final String FILEVERSION = "66ea47f7da884dff1c42ccede75113971c5c11f3"; // 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/gui/util/textarea.properties b/src/core/src/main/resources/org/apache/jmeter/gui/util/textarea.properties
index 76b5365..f587f3b 100644
--- a/src/core/src/main/resources/org/apache/jmeter/gui/util/textarea.properties
+++ b/src/core/src/main/resources/org/apache/jmeter/gui/util/textarea.properties
@@ -37,6 +37,7 @@ jexl3 = text/java
jpython = text/python
js = text/javascript
jscript = text/javascript
+json = text/json
judoscript = text/plain
jython = text/python
lisp = text/lisp
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 9013ed1..903e9db 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
@@ -293,6 +293,7 @@ deltest=Deletion test
deref=Dereference aliases
description=Description
detail=Detail
+detect_graphql_request=Detect GraphQL Request
directory_field_title=Working directory:
disable=Disable
dn=DN
@@ -449,6 +450,11 @@ graph_results_ms=ms
graph_results_no_samples=No of Samples
graph_results_throughput=Throughput
graph_results_title=Graph Results
+graphql_http_sampler_title=GraphQL HTTP Request
+graphql_request_info=GraphQL Request
+graphql_operation_name=Operation Name
+graphql_query=Query
+graphql_variables=Variables
groovy_function_expression=Expression to evaluate
grouping_add_separators=Add separators between groups
grouping_in_controllers=Put each group in a new controller
@@ -864,6 +870,7 @@ proxy_headers=Capture HTTP Headers
proxy_pause_http_sampler=Create new transaction after request (ms)\:
proxy_recorder_dialog=Recorder\: Transactions Control
proxy_regex=Regex matching
+proxy_sampler_graphql_settings=GraphQL HTTP Sampler settings
proxy_sampler_settings=HTTP Sampler settings
proxy_sampler_type=Type\:
proxy_separators=Add Separators
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 0dc5426..ddb10a2 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
@@ -288,6 +288,7 @@ deltest=Suppression
deref=Déréférencement des alias
description=Description
detail=Détail
+detect_graphql_request=Détecter les requêtes GraphQL
directory_field_title=Répertoire d'exécution \:
disable=Désactiver
dn=Racine DN \:
@@ -443,6 +444,11 @@ graph_results_ms=ms
graph_results_no_samples=Nombre d'échantillons
graph_results_throughput=Débit
graph_results_title=Graphique de résultats
+graphql_http_sampler_title=Requête HTTP GraphQL
+graphql_request_info=Requête GraphQL
+graphql_operation_name=Nom de l'opération
+graphql_query=Requête
+graphql_variables=Variables
groovy_function_expression=Expression à évaluer
grouping_add_separators=Ajouter des séparateurs entre les groupes
grouping_in_controllers=Mettre chaque groupe dans un nouveau contrôleur
@@ -853,6 +859,7 @@ proxy_headers=Capturer les entêtes HTTP
proxy_pause_http_sampler=Créer une nouvelle transaction après la requête (ms) \:
proxy_recorder_dialog=Enregistreur\: Contrôle des transactions
proxy_regex=Correspondance des variables par regex ?
+proxy_sampler_graphql_settings=Configuration de la requête GraphQL
proxy_sampler_settings=Paramètres Echantillon HTTP
proxy_sampler_type=Type \:
proxy_separators=Ajouter des séparateurs
diff --git a/src/protocol/build.gradle.kts b/src/protocol/build.gradle.kts
index aca2843..d0855cf 100644
--- a/src/protocol/build.gradle.kts
+++ b/src/protocol/build.gradle.kts
@@ -85,6 +85,8 @@ project("http") {
implementation("org.apache.httpcomponents:httpcore")
implementation("org.brotli:dec")
implementation("com.miglayout:miglayout-swing")
+ implementation("com.fasterxml.jackson.core:jackson-core")
+ implementation("com.fasterxml.jackson.core:jackson-databind")
testImplementation(testFixtures(project(":src:testkit-wiremock")))
testImplementation("com.github.tomakehurst:wiremock-jre8")
}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/GraphQLRequestParams.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/GraphQLRequestParams.java
new file mode 100644
index 0000000..6edabbf
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/GraphQLRequestParams.java
@@ -0,0 +1,67 @@
+/*
+ * 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.protocol.http.config;
+
+import java.io.Serializable;
+
+/**
+ * Represents GraphQL request parameter input data for Query, Variables and Operation Name.
+ */
+public class GraphQLRequestParams implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ private String operationName;
+
+ private String query;
+
+ private String variables;
+
+ public GraphQLRequestParams() {
+ }
+
+ public GraphQLRequestParams(final String operationName, final String query, final String variables) {
+ this.operationName = operationName;
+ this.query = query;
+ this.variables = variables;
+ }
+
+ public String getOperationName() {
+ return operationName;
+ }
+
+ public void setOperationName(String operationName) {
+ this.operationName = operationName;
+ }
+
+ public String getQuery() {
+ return query;
+ }
+
+ public void setQuery(String query) {
+ this.query = query;
+ }
+
+ public String getVariables() {
+ return variables;
+ }
+
+ public void setVariables(String variables) {
+ this.variables = variables;
+ }
+}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/AbstractValidationTabbedPane.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/AbstractValidationTabbedPane.java
new file mode 100644
index 0000000..bc8c942
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/AbstractValidationTabbedPane.java
@@ -0,0 +1,86 @@
+/*
+ * 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.protocol.http.config.gui;
+
+import javax.swing.JTabbedPane;
+
+/**
+ * Abstract {@link JTabbedPane} to allow validating the requested tab index, updating states and changing the tab index
+ * after the validation if necessary.
+ */
+abstract class AbstractValidationTabbedPane extends JTabbedPane {
+
+ private static final long serialVersionUID = 7014311238367882880L;
+
+ /**
+ * Flag whether the validation feature should be enabled or not, {@code true} by default.
+ */
+ private boolean validationEnabled = true;
+
+ /**
+ * {@inheritDoc}
+ * <P>
+ * Overridden to delegate to {@link #setSelectedIndex(int, boolean)} in order to validate the requested tab index by default.
+ */
+ @Override
+ public void setSelectedIndex(int index) {
+ setSelectedIndex(index, true);
+ }
+
+ /**
+ * Apply some check rules by invoking {@link #getValidatedTabIndex(int, int)}
+ * if {@link #isValidationEnabled()} returns true and the {@code check} input is true.
+ *
+ * @param index index to select
+ * @param check flag whether to perform checks before setting the selected index
+ */
+ public void setSelectedIndex(int index, boolean check) {
+ final int curIndex = super.getSelectedIndex();
+
+ if (!isValidationEnabled() || !check || curIndex == -1) {
+ super.setSelectedIndex(index);
+ return;
+ }
+
+ super.setSelectedIndex(getValidatedTabIndex(curIndex, index));
+ }
+
+ /**
+ * Validate the requested tab index ({@code newTabIndex}) and return a validated tab index after applying some check rules.
+ * @param currentTabIndex current tab index
+ * @param newTabIndex new requested tab index to validate
+ * @return the validated tab index
+ */
+ abstract protected int getValidatedTabIndex(final int currentTabIndex, final int newTabIndex);
+
+ /**
+ * Return true if the validation feature should be enabled, {@code true} by default.
+ * @return true if the validation feature should be enabled, {@code true} by default
+ */
+ protected boolean isValidationEnabled() {
+ return validationEnabled;
+ }
+
+ /**
+ * Set the flag whether the validation feature should be enabled or not.
+ * @param validationEnabled flag whether the validation feature should be enabled or not
+ */
+ protected void setValidationEnabled(boolean validationEnabled) {
+ this.validationEnabled = validationEnabled;
+ }
+}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/GraphQLUrlConfigGui.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/GraphQLUrlConfigGui.java
new file mode 100644
index 0000000..38b3824
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/GraphQLUrlConfigGui.java
@@ -0,0 +1,190 @@
+/*
+ * 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.protocol.http.config.gui;
+
+import java.awt.Component;
+
+import javax.swing.BorderFactory;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jmeter.config.Arguments;
+import org.apache.jmeter.gui.util.HorizontalPanel;
+import org.apache.jmeter.gui.util.JSyntaxTextArea;
+import org.apache.jmeter.gui.util.JTextScrollPane;
+import org.apache.jmeter.protocol.http.config.GraphQLRequestParams;
+import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
+import org.apache.jmeter.protocol.http.util.GraphQLRequestParamUtils;
+import org.apache.jmeter.protocol.http.util.HTTPArgument;
+import org.apache.jmeter.protocol.http.util.HTTPConstants;
+import org.apache.jmeter.testelement.TestElement;
+import org.apache.jmeter.testelement.property.TestElementProperty;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jorphan.gui.JLabeledTextField;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Extending {@link UrlConfigGui}, GraphQL over HTTP Request configuration GUI, providing more convenient UI elements
+ * for GraphQL query, variables and operationName.
+ */
+public class GraphQLUrlConfigGui extends UrlConfigGui {
+
+ private static final long serialVersionUID = 1L;
+
+ private static Logger log = LoggerFactory.getLogger(GraphQLUrlConfigGui.class);
+
+ public static final String OPERATION_NAME = "GraphQLHTTPSampler.operationName";
+
+ public static final String QUERY = "GraphQLHTTPSampler.query";
+
+ public static final String VARIABLES = "GraphQLHTTPSampler.variables";
+
+ /**
+ * Default value settings for GraphQL URL Configuration GUI elements.
+ */
+ private static final UrlConfigDefaults URL_CONFIG_DEFAULTS = new UrlConfigDefaults();
+ static {
+ URL_CONFIG_DEFAULTS.setValidMethods(new String[] { HTTPConstants.POST, HTTPConstants.GET });
+ URL_CONFIG_DEFAULTS.setDefaultMethod(HTTPConstants.POST);
+ URL_CONFIG_DEFAULTS.setAutoRedirects(false);
+ URL_CONFIG_DEFAULTS.setFollowRedirects(false);
+ URL_CONFIG_DEFAULTS.setUseBrowserCompatibleMultipartMode(false);
+ URL_CONFIG_DEFAULTS.setUseKeepAlive(true);
+ URL_CONFIG_DEFAULTS.setUseMultipart(false);
+ URL_CONFIG_DEFAULTS.setUseMultipartVisible(false);
+ }
+
+ private JLabeledTextField operationNameText;
+
+ private JSyntaxTextArea queryContent;
+
+ private JSyntaxTextArea variablesContent;
+
+ /**
+ * Constructor which is setup to show the sampler fields for GraphQL over HTTP request.
+ */
+ public GraphQLUrlConfigGui() {
+ super(true, false, false);
+ }
+
+ @Override
+ public void configure(TestElement element) {
+ super.configure(element);
+ final String operationName = element.getPropertyAsString(OPERATION_NAME, "");
+ operationNameText.setText(operationName);
+ final String query = element.getPropertyAsString(QUERY, "");
+ queryContent.setText(query);
+ final String variables = element.getPropertyAsString(VARIABLES, "");
+ variablesContent.setText(variables);
+ }
+
+ @Override
+ public void modifyTestElement(TestElement element) {
+ super.modifyTestElement(element);
+
+ final String method = element.getPropertyAsString(HTTPSamplerBase.METHOD);
+ final GraphQLRequestParams params = new GraphQLRequestParams(operationNameText.getText(),
+ queryContent.getText(), variablesContent.getText());
+
+ element.setProperty(OPERATION_NAME, params.getOperationName());
+ element.setProperty(QUERY, params.getQuery());
+ element.setProperty(VARIABLES, params.getVariables());
+ element.setProperty(HTTPSamplerBase.POST_BODY_RAW, !HTTPConstants.GET.equals(method));
+
+ final Arguments args;
+
+ if (HTTPConstants.GET.equals(method)) {
+ args = createHTTPArgumentsTestElement();
+
+ if (StringUtils.isNotBlank(params.getOperationName())) {
+ args.addArgument(createHTTPArgument("operationName", params.getOperationName().trim(), true));
+ }
+
+ args.addArgument(createHTTPArgument("query",
+ GraphQLRequestParamUtils.queryToGetParamValue(params.getQuery()), true));
+
+ if (StringUtils.isNotBlank(params.getVariables())) {
+ final String variablesParamValue = GraphQLRequestParamUtils
+ .variablesToGetParamValue(params.getVariables());
+ if (variablesParamValue != null) {
+ args.addArgument(createHTTPArgument("variables", variablesParamValue, true));
+ }
+ }
+ } else {
+ args = new Arguments();
+ args.addArgument(createHTTPArgument("", GraphQLRequestParamUtils.toPostBodyString(params), false));
+ }
+
+ element.setProperty(new TestElementProperty(HTTPSamplerBase.ARGUMENTS, args));
+ }
+
+ @Override
+ protected UrlConfigDefaults getUrlConfigDefaults() {
+ return URL_CONFIG_DEFAULTS;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <P>
+ * Overridden to add the extra GraphQL Request Information section including 'operationName' text field.
+ */
+ @Override
+ protected Component getPathPanel() {
+ final JPanel panel = (JPanel) super.getPathPanel();
+ JPanel graphQLReqInfoPane = new HorizontalPanel();
+ graphQLReqInfoPane
+ .setBorder(BorderFactory.createTitledBorder(JMeterUtils.getResString("graphql_request_info")));
+ operationNameText = new JLabeledTextField(JMeterUtils.getResString("graphql_operation_name"), 40);
+ graphQLReqInfoPane.add(operationNameText);
+ panel.add(graphQLReqInfoPane);
+ return panel;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <P>
+ * Overridden to remove the existing tab for parameter arguments and GraphQL variables content pane.
+ */
+ @Override
+ protected JTabbedPane getParameterPanel() {
+ final AbstractValidationTabbedPane paramPanel = (AbstractValidationTabbedPane) super.getParameterPanel();
+ paramPanel.removeAll();
+ paramPanel.setValidationEnabled(false);
+
+ queryContent = JSyntaxTextArea.getInstance(26, 50);
+ queryContent.setInitialText("");
+ paramPanel.add(JMeterUtils.getResString("graphql_query"), JTextScrollPane.getInstance(queryContent));
+
+ variablesContent = JSyntaxTextArea.getInstance(26, 50);
+ variablesContent.setLanguage("json");
+ variablesContent.setInitialText("");
+ paramPanel.add(JMeterUtils.getResString("graphql_variables"), JTextScrollPane.getInstance(variablesContent));
+
+ return paramPanel;
+ }
+
+ private HTTPArgument createHTTPArgument(final String name, final String value, final boolean alwaysEncoded) {
+ final HTTPArgument arg = new HTTPArgument(name, value);
+ arg.setUseEquals(true);
+ arg.setEnabled(true);
+ arg.setAlwaysEncoded(alwaysEncoded);
+ return arg;
+ }
+}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigDefaults.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigDefaults.java
new file mode 100644
index 0000000..e830628
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigDefaults.java
@@ -0,0 +1,272 @@
+/*
+ * 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.protocol.http.config.gui;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
+
+/**
+ * Default option value settings for {@link UrlConfigGui}.
+ */
+public class UrlConfigDefaults implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Available HTTP methods to be shown in the {@link UrlConfigGui}.
+ */
+ private List<String> validMethodList;
+
+ /**
+ * The default HTTP method to be selected in the {@link UrlConfigGui}.
+ */
+ private String defaultMethod = HTTPSamplerBase.DEFAULT_METHOD;
+
+ /**
+ * The default value to be set for the followRedirect checkbox in the {@link UrlConfigGui}.
+ */
+ private boolean followRedirects = true;
+
+ /**
+ * The default value to be set for the autoRedirects checkbox in the {@link UrlConfigGui}.
+ */
+ private boolean autoRedirects;
+
+ /**
+ * The default value to be set for the useKeepAlive checkbox in the {@link UrlConfigGui}.
+ */
+ private boolean useKeepAlive = true;
+
+ /**
+ * The default value to be set for the useMultipart checkbox in the {@link UrlConfigGui}.
+ */
+ private boolean useMultipart;
+
+ /**
+ * The default value to be set for the useBrowserCompatibleMultipartMode checkbox in the {@link UrlConfigGui}.
+ */
+ private boolean useBrowserCompatibleMultipartMode = HTTPSamplerBase.BROWSER_COMPATIBLE_MULTIPART_MODE_DEFAULT;
+
+ /**
+ * Flag whether to show the followRedirect checkbox in the {@link UrlConfigGui}.
+ */
+ private boolean followRedirectsVisible = true;
+
+ /**
+ * Flag whether to show the autoRedirectsVisible checkbox in the {@link UrlConfigGui}.
+ */
+ private boolean autoRedirectsVisible = true;
+
+ /**
+ * Flag whether to show the useKeepAliveVisible checkbox in the {@link UrlConfigGui}.
+ */
+ private boolean useKeepAliveVisible = true;
+
+ /**
+ * Flag whether to show the useMultipartVisible checkbox in the {@link UrlConfigGui}.
+ */
+ private boolean useMultipartVisible = true;
+
+ /**
+ * Flag whether to show the useBrowserCompatibleMultipartModeVisible checkbox in the {@link UrlConfigGui}.
+ */
+ private boolean useBrowserCompatibleMultipartModeVisible = true;
+
+ /**
+ * Return available HTTP methods to be shown in the {@link UrlConfigGui}, returning {@link HTTPSamplerBase#getValidMethodsAsArray()}
+ * by default if not reset.
+ * @return available HTTP methods to be shown in the {@link UrlConfigGui}
+ */
+ public String[] getValidMethods() {
+ if (validMethodList != null) {
+ return validMethodList.toArray(new String[validMethodList.size()]);
+ }
+ return HTTPSamplerBase.getValidMethodsAsArray();
+ }
+
+ /**
+ * Set available HTTP methods to be shown in the {@link UrlConfigGui}.
+ * @param validMethods available HTTP methods
+ * @throws IllegalArgumentException if the input array is empty
+ */
+ public void setValidMethods(String[] validMethods) {
+ if (validMethods == null || validMethods.length == 0) {
+ throw new IllegalArgumentException("HTTP methods array is empty.");
+ }
+ this.validMethodList = Arrays.asList(validMethods);
+ }
+
+ /**
+ * Return the default HTTP method to be selected in the {@link UrlConfigGui}.
+ * @return the default HTTP method to be selected in the {@link UrlConfigGui}
+ */
+ public String getDefaultMethod() {
+ return defaultMethod;
+ }
+
+ /**
+ * Set the default HTTP method to be selected in the {@link UrlConfigGui}.
+ * @param defaultMethod the default HTTP method to be selected in the {@link UrlConfigGui}
+ */
+ public void setDefaultMethod(String defaultMethod) {
+ this.defaultMethod = defaultMethod;
+ }
+
+ /**
+ * Return the default value to be set for the followRedirect checkbox in the {@link UrlConfigGui}.
+ */
+ public boolean isFollowRedirects() {
+ return followRedirects;
+ }
+
+ /**
+ * Set the default value to be set for the followRedirect checkbox in the {@link UrlConfigGui}.
+ */
+ public void setFollowRedirects(boolean followRedirects) {
+ this.followRedirects = followRedirects;
+ }
+
+ /**
+ * Return the default value to be set for the autoRedirects checkbox in the {@link UrlConfigGui}.
+ */
+ public boolean isAutoRedirects() {
+ return autoRedirects;
+ }
+
+ /**
+ * Set the default value to be set for the autoRedirects checkbox in the {@link UrlConfigGui}.
+ */
+ public void setAutoRedirects(boolean autoRedirects) {
+ this.autoRedirects = autoRedirects;
+ }
+
+ /**
+ * Return the default value to be set for the useKeepAlive checkbox in the {@link UrlConfigGui}.
+ */
+ public boolean isUseKeepAlive() {
+ return useKeepAlive;
+ }
+
+ /**
+ * Set the default value to be set for the useKeepAlive checkbox in the {@link UrlConfigGui}.
+ */
+ public void setUseKeepAlive(boolean useKeepAlive) {
+ this.useKeepAlive = useKeepAlive;
+ }
+
+ /**
+ * Return the default value to be set for the useMultipart checkbox in the {@link UrlConfigGui}.
+ */
+ public boolean isUseMultipart() {
+ return useMultipart;
+ }
+
+ /**
+ * Set the default value to be set for the useMultipart checkbox in the {@link UrlConfigGui}.
+ */
+ public void setUseMultipart(boolean useMultipart) {
+ this.useMultipart = useMultipart;
+ }
+
+ /**
+ * Return the default value to be set for the useBrowserCompatibleMultipartMode checkbox in the {@link UrlConfigGui}.
+ */
+ public boolean isUseBrowserCompatibleMultipartMode() {
+ return useBrowserCompatibleMultipartMode;
+ }
+
+ /**
+ * Set the default value to be set for the useBrowserCompatibleMultipartMode checkbox in the {@link UrlConfigGui}.
+ */
+ public void setUseBrowserCompatibleMultipartMode(boolean useBrowserCompatibleMultipartMode) {
+ this.useBrowserCompatibleMultipartMode = useBrowserCompatibleMultipartMode;
+ }
+
+ /**
+ * Return true if the followRedirect checkbox should be visible in the {@link UrlConfigGui}.
+ */
+ public boolean isFollowRedirectsVisible() {
+ return followRedirectsVisible;
+ }
+
+ /**
+ * Set the visibility of the followRedirect checkbox in the {@link UrlConfigGui}.
+ */
+ public void setFollowRedirectsVisible(boolean followRedirectsVisible) {
+ this.followRedirectsVisible = followRedirectsVisible;
+ }
+
+ /**
+ * Return true if the autoRedirectsVisible checkbox should be visible in the {@link UrlConfigGui}.
+ */
+ public boolean isAutoRedirectsVisible() {
+ return autoRedirectsVisible;
+ }
+
+ /**
+ * Set the visibility of the autoRedirectsVisible checkbox in the {@link UrlConfigGui}.
+ */
+ public void setAutoRedirectsVisible(boolean autoRedirectsVisible) {
+ this.autoRedirectsVisible = autoRedirectsVisible;
+ }
+
+ /**
+ * Return true if the useKeepAliveVisible checkbox should be visible in the {@link UrlConfigGui}.
+ */
+ public boolean isUseKeepAliveVisible() {
+ return useKeepAliveVisible;
+ }
+
+ /**
+ * Set the visibility of the useKeepAliveVisible checkbox in the {@link UrlConfigGui}.
+ */
+ public void setUseKeepAliveVisible(boolean useKeepAliveVisible) {
+ this.useKeepAliveVisible = useKeepAliveVisible;
+ }
+
+ /**
+ * Return true if the useMultipartVisible checkbox should by default in the {@link UrlConfigGui}.
+ */
+ public boolean isUseMultipartVisible() {
+ return useMultipartVisible;
+ }
+
+ /**
+ * Set the visibility of the useMultipartVisible checkbox in the {@link UrlConfigGui}.
+ */
+ public void setUseMultipartVisible(boolean useMultipartVisible) {
+ this.useMultipartVisible = useMultipartVisible;
+ }
+
+ /**
+ * Return true if the useBrowserCompatibleMultipartModeVisible checkbox should be visible in the {@link UrlConfigGui}.
+ */
+ public boolean isUseBrowserCompatibleMultipartModeVisible() {
+ return useBrowserCompatibleMultipartModeVisible;
+ }
+
+ /**
+ * Set the visibility of the useBrowserCompatibleMultipartModeVisible checkbox in the {@link UrlConfigGui}.
+ */
+ public void setUseBrowserCompatibleMultipartModeVisible(boolean useBrowserCompatibleMultipartModeVisible) {
+ this.useBrowserCompatibleMultipartModeVisible = useBrowserCompatibleMultipartModeVisible;
+ }
+}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java
index 2fd4a69..4f29b4b 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/config/gui/UrlConfigGui.java
@@ -63,6 +63,11 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
private static final long serialVersionUID = 240L;
+ /**
+ * Default value settings for URL Configuration GUI elements.
+ */
+ private static final UrlConfigDefaults URL_CONFIG_DEFAULTS = new UrlConfigDefaults();
+
private static final int TAB_PARAMETERS = 0;
private int tabRawBodyIndex = 1;
@@ -106,7 +111,7 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
private JSyntaxTextArea postBodyContent;
// Tabbed pane that contains parameters and raw body
- private ValidationTabbedPane postContentTabbedPane;
+ private AbstractValidationTabbedPane postContentTabbedPane;
private boolean showRawBodyPane;
private boolean showFileUploadPane;
@@ -156,12 +161,12 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
public void clear() {
domain.setText(""); // $NON-NLS-1$
if (notConfigOnly){
- followRedirects.setSelected(true);
- autoRedirects.setSelected(false);
- method.setText(HTTPSamplerBase.DEFAULT_METHOD);
- useKeepAlive.setSelected(true);
- useMultipart.setSelected(false);
- useBrowserCompatibleMultipartMode.setSelected(HTTPSamplerBase.BROWSER_COMPATIBLE_MULTIPART_MODE_DEFAULT);
+ followRedirects.setSelected(getUrlConfigDefaults().isFollowRedirects());
+ autoRedirects.setSelected(getUrlConfigDefaults().isAutoRedirects());
+ method.setText(getUrlConfigDefaults().getDefaultMethod());
+ useKeepAlive.setSelected(getUrlConfigDefaults().isUseKeepAlive());
+ useMultipart.setSelected(getUrlConfigDefaults().isUseMultipart());
+ useBrowserCompatibleMultipartMode.setSelected(getUrlConfigDefaults().isUseBrowserCompatibleMultipartMode());
}
path.setText(""); // $NON-NLS-1$
port.setText(""); // $NON-NLS-1$
@@ -193,7 +198,7 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
* @param element {@link TestElement} to modify
*/
public void modifyTestElement(TestElement element) {
- boolean useRaw = !postBodyContent.getText().isEmpty();
+ boolean useRaw = showRawBodyPane && !postBodyContent.getText().isEmpty();
Arguments args;
if(useRaw) {
args = new Arguments();
@@ -273,18 +278,21 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
setName(el.getName());
Arguments arguments = (Arguments) el.getProperty(HTTPSamplerBase.ARGUMENTS).getObjectValue();
- boolean useRaw = el.getPropertyAsBoolean(HTTPSamplerBase.POST_BODY_RAW, HTTPSamplerBase.POST_BODY_RAW_DEFAULT);
- if(useRaw) {
- String postBody = computePostBody(arguments, true); // Convert CRLF to CR, see modifyTestElement
- postBodyContent.setInitialText(postBody);
- postBodyContent.setCaretPosition(0);
- argsPanel.clear();
- postContentTabbedPane.setSelectedIndex(tabRawBodyIndex, false);
- } else {
- postBodyContent.setInitialText("");
- argsPanel.configure(arguments);
- postContentTabbedPane.setSelectedIndex(TAB_PARAMETERS, false);
+ if (showRawBodyPane) {
+ boolean useRaw = el.getPropertyAsBoolean(HTTPSamplerBase.POST_BODY_RAW, HTTPSamplerBase.POST_BODY_RAW_DEFAULT);
+ if(useRaw) {
+ String postBody = computePostBody(arguments, true); // Convert CRLF to CR, see modifyTestElement
+ postBodyContent.setInitialText(postBody);
+ postBodyContent.setCaretPosition(0);
+ argsPanel.clear();
+ postContentTabbedPane.setSelectedIndex(tabRawBodyIndex, false);
+ } else {
+ postBodyContent.setInitialText("");
+ argsPanel.configure(arguments);
+ postContentTabbedPane.setSelectedIndex(TAB_PARAMETERS, false);
+ }
}
+
if(showFileUploadPane) {
filesPanel.configure(el);
}
@@ -349,6 +357,13 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
return webServerPanel;
}
+ /**
+ * Return the {@link UrlConfigDefaults} instance to be used when configuring the UI elements and default values.
+ * @return the {@link UrlConfigDefaults} instance to be used when configuring the UI elements and default values
+ */
+ protected UrlConfigDefaults getUrlConfigDefaults() {
+ return URL_CONFIG_DEFAULTS;
+ }
/**
* This method defines the Panel for:
@@ -365,33 +380,37 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
if (notConfigOnly){
method = new JLabeledChoice(JMeterUtils.getResString("method"), // $NON-NLS-1$
- HTTPSamplerBase.getValidMethodsAsArray(), true, false);
+ getUrlConfigDefaults().getValidMethods(), true, false);
method.addChangeListener(this);
}
if (notConfigOnly){
followRedirects = new JCheckBox(JMeterUtils.getResString("follow_redirects")); // $NON-NLS-1$
JFactory.small(followRedirects);
- followRedirects.setSelected(true);
+ followRedirects.setSelected(getUrlConfigDefaults().isFollowRedirects());
followRedirects.addChangeListener(this);
+ followRedirects.setVisible(getUrlConfigDefaults().isFollowRedirectsVisible());
autoRedirects = new JCheckBox(JMeterUtils.getResString("follow_redirects_auto")); //$NON-NLS-1$
JFactory.small(autoRedirects);
autoRedirects.addChangeListener(this);
- autoRedirects.setSelected(false);// Default changed in 2.3 and again in 2.4
+ autoRedirects.setSelected(getUrlConfigDefaults().isAutoRedirects());// Default changed in 2.3 and again in 2.4
+ autoRedirects.setVisible(getUrlConfigDefaults().isAutoRedirectsVisible());
useKeepAlive = new JCheckBox(JMeterUtils.getResString("use_keepalive")); // $NON-NLS-1$
JFactory.small(useKeepAlive);
- useKeepAlive.setSelected(true);
+ useKeepAlive.setSelected(getUrlConfigDefaults().isUseKeepAlive());
+ useKeepAlive.setVisible(getUrlConfigDefaults().isUseKeepAliveVisible());
useMultipart = new JCheckBox(JMeterUtils.getResString("use_multipart_for_http_post")); // $NON-NLS-1$
JFactory.small(useMultipart);
- useMultipart.setSelected(false);
+ useMultipart.setSelected(getUrlConfigDefaults().isUseMultipart());
+ useMultipart.setVisible(getUrlConfigDefaults().isUseMultipartVisible());
useBrowserCompatibleMultipartMode = new JCheckBox(JMeterUtils.getResString("use_multipart_mode_browser")); // $NON-NLS-1$
JFactory.small(useBrowserCompatibleMultipartMode);
- useBrowserCompatibleMultipartMode.setSelected(HTTPSamplerBase.BROWSER_COMPATIBLE_MULTIPART_MODE_DEFAULT);
-
+ useBrowserCompatibleMultipartMode.setSelected(getUrlConfigDefaults().isUseBrowserCompatibleMultipartMode());
+ useBrowserCompatibleMultipartMode.setVisible(getUrlConfigDefaults().isUseBrowserCompatibleMultipartModeVisible());
}
JPanel pathPanel = new HorizontalPanel();
@@ -438,59 +457,47 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
}
/**
- *
+ * Create a new {@link Arguments} instance associated with the specific GUI used in this component.
+ * @return a new {@link Arguments} instance associated with the specific GUI used in this component
*/
- class ValidationTabbedPane extends JTabbedPane {
+ protected Arguments createHTTPArgumentsTestElement() {
+ return (Arguments) argsPanel.createTestElement();
+ }
- /**
- *
- */
- private static final long serialVersionUID = 7014311238367882880L;
+ class ValidationTabbedPane extends AbstractValidationTabbedPane {
+ private static final long serialVersionUID = 7014311238367882881L;
@Override
- public void setSelectedIndex(int index) {
- setSelectedIndex(index, true);
- }
-
- /**
- * Apply some check rules if check is true
- *
- * @param index
- * index to select
- * @param check
- * flag whether to perform checks before setting the selected
- * index
- */
- public void setSelectedIndex(int index, boolean check) {
- int oldSelectedIndex = this.getSelectedIndex();
- if(!check || oldSelectedIndex == -1) {
- super.setSelectedIndex(index);
- } else if(index == tabFileUploadIndex) { // We're going to File, no problem
- super.setSelectedIndex(index);
+ protected int getValidatedTabIndex(int currentTabIndex, int newTabIndex) {
+ if (newTabIndex == tabFileUploadIndex) { // We're going to File, no problem
+ return newTabIndex;
}
+
// We're moving to Raw or Parameters
- else if(index != oldSelectedIndex) {
+ if (newTabIndex != currentTabIndex) {
// If the Parameter data can be converted (i.e. no names)
// we switch
- if(index == tabRawBodyIndex) {
- if(canSwitchToRawBodyPane()) {
+ if (newTabIndex == tabRawBodyIndex) {
+ if (canSwitchToRawBodyPane()) {
convertParametersToRaw();
- super.setSelectedIndex(index);
+ return newTabIndex;
} else {
- super.setSelectedIndex(TAB_PARAMETERS);
+ return TAB_PARAMETERS;
}
}
else {
// If the Parameter data cannot be converted to Raw, then the user should be
// prevented from doing so raise an error dialog
- if(canSwitchToParametersTab()) {
- super.setSelectedIndex(index);
+ if (canSwitchToParametersTab()) {
+ return newTabIndex;
} else {
- super.setSelectedIndex(tabRawBodyIndex);
+ return tabRawBodyIndex;
}
}
}
+
+ return newTabIndex;
}
/**
@@ -510,7 +517,7 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
* @return true if postBodyContent is empty
*/
private boolean canSwitchToParametersTab() {
- return postBodyContent.getText().isEmpty();
+ return showRawBodyPane && postBodyContent.getText().isEmpty();
}
}
@@ -531,7 +538,7 @@ public class UrlConfigGui extends JPanel implements ChangeListener {
* Convert Parameters to Raw Body
*/
void convertParametersToRaw() {
- if(postBodyContent.getText().isEmpty()) {
+ if (showRawBodyPane && postBodyContent.getText().isEmpty()) {
postBodyContent.setInitialText(computePostBody((Arguments)argsPanel.createTestElement()));
postBodyContent.setCaretPosition(0);
}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HeaderManager.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HeaderManager.java
index aaf675a..02d5b39 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HeaderManager.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HeaderManager.java
@@ -200,6 +200,26 @@ public class HeaderManager extends ConfigTestElement implements Serializable, Re
}
/**
+ * Get the first header from Headers by the header name, or {@code null} if not found.
+ * @param name header name
+ * @return the first header from Headers by the header name, or {@code null} if not found
+ */
+ public Header getFirstHeaderNamed(final String name) {
+ final CollectionProperty headers = getHeaders();
+ final int size = headers.size();
+ for (int i = 0; i < size; i++) {
+ Header header = (Header) headers.get(i).getObjectValue();
+ if (header == null) {
+ continue;
+ }
+ if (header.getName().equalsIgnoreCase(name)) {
+ return header;
+ }
+ }
+ return null;
+ }
+
+ /**
* Remove from Headers the header named name
* @param name header name
*/
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/GraphQLHTTPSamplerGui.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/GraphQLHTTPSamplerGui.java
new file mode 100644
index 0000000..c393e77
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/GraphQLHTTPSamplerGui.java
@@ -0,0 +1,75 @@
+/*
+ * 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.protocol.http.control.gui;
+
+import javax.swing.JPanel;
+
+import org.apache.jmeter.gui.TestElementMetadata;
+import org.apache.jmeter.protocol.http.config.gui.GraphQLUrlConfigGui;
+import org.apache.jmeter.protocol.http.config.gui.UrlConfigGui;
+import org.apache.jmeter.util.JMeterUtils;
+
+/**
+ * GraphQL HTTP Sampler GUI which extends {@link HttpTestSampleGui} in order to provide more convenient UI elements for
+ * GraphQL query, variables and operationName.
+ */
+@TestElementMetadata(labelResource = "graphql_http_sampler_title")
+public class GraphQLHTTPSamplerGui extends HttpTestSampleGui {
+
+ private static final long serialVersionUID = 1L;
+
+ public GraphQLHTTPSamplerGui() {
+ super();
+ }
+
+ // Use this instead of getLabelResource() otherwise getDocAnchor() below does not work
+ @Override
+ public String getStaticLabel() {
+ return JMeterUtils.getResString("graphql_http_sampler_title"); // $NON-NLS-1$
+ }
+
+ @Override
+ public String getDocAnchor() {// reuse documentation
+ return super.getStaticLabel().replace(' ', '_'); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ /**
+ * {@inheritDoc}
+ * <P>
+ * Overridden to hide the HTML embedded resource handling section as GraphQL responses are always in JSON.
+ */
+ @Override
+ protected JPanel createEmbeddedRsrcPanel() {
+ final JPanel panel = super.createEmbeddedRsrcPanel();
+ // No need to consider embedded resources in HTML as the GraphQL responses are always in JSON.
+ panel.setVisible(false);
+ return panel;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <P>
+ * Overridden to create a {@link GraphQLUrlConfigGui} which extends {@link UrlConfigGui} for GraphQL specific UI elements.
+ */
+ @Override
+ protected UrlConfigGui createUrlConfigGui() {
+ final GraphQLUrlConfigGui configGui = new GraphQLUrlConfigGui();
+ configGui.setBorder(makeBorder());
+ return configGui;
+ }
+}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/HttpTestSampleGui.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/HttpTestSampleGui.java
index f7acf2a..0464568 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/HttpTestSampleGui.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/gui/HttpTestSampleGui.java
@@ -168,10 +168,52 @@ public class HttpTestSampleGui extends AbstractSamplerGui {
setLayout(new BorderLayout(0, 5));
setBorder(BorderFactory.createEmptyBorder());
+ JTabbedPane tabbedPane = createTabbedConfigPane();
+
+ JPanel wrapper = new JPanel(new BorderLayout());
+ wrapper.setBorder(makeBorder());
+ wrapper.add(makeTitlePanel(), BorderLayout.CENTER);
+
+ JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, wrapper, tabbedPane);
+ splitPane.setBorder(BorderFactory.createEmptyBorder());
+ splitPane.setOneTouchExpandable(true);
+ add(splitPane);
+ }
+
+ /**
+ * Create the parameters configuration tabstrip which includes the Basic tab ({@link UrlConfigGui})
+ * and the Advanced tab by default.
+ * @return the parameters configuration tabstrip which includes the Basic tab ({@link UrlConfigGui})
+ * and the Advanced tab by default
+ */
+ protected JTabbedPane createTabbedConfigPane() {
+ final JTabbedPane tabbedPane = new JTabbedPane();
+
// URL CONFIG
- urlConfigGui = new UrlConfigGui(true, true, true);
- urlConfigGui.setBorder(makeBorder());
+ urlConfigGui = createUrlConfigGui();
+
+ tabbedPane.add(JMeterUtils
+ .getResString("web_testing_basic"), urlConfigGui);
+
+ // AdvancedPanel (embedded resources, source address and optional tasks)
+ final JPanel advancedPanel = createAdvancedConfigPanel();
+ tabbedPane.add(JMeterUtils
+ .getResString("web_testing_advanced"), advancedPanel);
+ return tabbedPane;
+ }
+
+ /**
+ * Create a {@link UrlConfigGui} which is used as the Basic tab in the parameters configuration tabstrip.
+ * @return a {@link UrlConfigGui} which is used as the Basic tab
+ */
+ protected UrlConfigGui createUrlConfigGui() {
+ final UrlConfigGui configGui = new UrlConfigGui(true, true, true);
+ configGui.setBorder(makeBorder());
+ return configGui;
+ }
+
+ private JPanel createAdvancedConfigPanel() {
// HTTP request options
JPanel httpOptions = new HorizontalPanel();
httpOptions.add(getImplementationPanel());
@@ -190,21 +232,7 @@ public class HttpTestSampleGui extends AbstractSamplerGui {
}
advancedPanel.add(createOptionalTasksPanel());
-
- JTabbedPane tabbedPane = new JTabbedPane();
- tabbedPane.add(JMeterUtils
- .getResString("web_testing_basic"), urlConfigGui);
- tabbedPane.add(JMeterUtils
- .getResString("web_testing_advanced"), advancedPanel);
-
- JPanel wrapper = new JPanel(new BorderLayout());
- wrapper.setBorder(makeBorder());
- wrapper.add(makeTitlePanel(), BorderLayout.CENTER);
-
- JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, wrapper, tabbedPane);
- splitPane.setBorder(BorderFactory.createEmptyBorder());
- splitPane.setOneTouchExpandable(true);
- add(splitPane);
+ return advancedPanel;
}
private JPanel getTimeOutPanel() {
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/DefaultSamplerCreator.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/DefaultSamplerCreator.java
index 2881a3f..61d1af5 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/DefaultSamplerCreator.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/DefaultSamplerCreator.java
@@ -33,12 +33,17 @@ import javax.xml.parsers.SAXParserFactory;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.Arguments;
+import org.apache.jmeter.protocol.http.config.GraphQLRequestParams;
import org.apache.jmeter.protocol.http.config.MultipartUrlConfig;
+import org.apache.jmeter.protocol.http.config.gui.GraphQLUrlConfigGui;
+import org.apache.jmeter.protocol.http.control.Header;
+import org.apache.jmeter.protocol.http.control.gui.GraphQLHTTPSamplerGui;
import org.apache.jmeter.protocol.http.control.gui.HttpTestSampleGui;
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerFactory;
import org.apache.jmeter.protocol.http.sampler.PostWriter;
import org.apache.jmeter.protocol.http.util.ConversionUtils;
+import org.apache.jmeter.protocol.http.util.GraphQLRequestParamUtils;
import org.apache.jmeter.protocol.http.util.HTTPConstants;
import org.apache.jmeter.protocol.http.util.HTTPFileArg;
import org.apache.jmeter.testelement.TestElement;
@@ -121,6 +126,45 @@ public class DefaultSamplerCreator extends AbstractSamplerCreator {
if(arguments.getArgumentCount() == 1 && arguments.getArgument(0).getName().length()==0) {
sampler.setPostBodyRaw(true);
}
+
+ if (request.isDetectGraphQLRequest()) {
+ detectAndModifySamplerOnGraphQLRequest(sampler, request);
+ }
+ }
+
+ private void detectAndModifySamplerOnGraphQLRequest(final HTTPSamplerBase sampler, final HttpRequestHdr request) {
+ final String method = request.getMethod();
+ final Header header = request.getHeaderManager().getFirstHeaderNamed("Content-Type");
+ final boolean graphQLContentType = header != null
+ && GraphQLRequestParamUtils.isGraphQLContentType(header.getValue());
+
+ GraphQLRequestParams params = null;
+
+ if (HTTPConstants.POST.equals(method) && graphQLContentType) {
+ try {
+ byte[] postData = request.getRawPostData();
+ if (postData != null && postData.length > 0) {
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams(request.getRawPostData(),
+ sampler.getContentEncoding());
+ }
+ } catch (Exception e) {
+ log.debug("Ignoring request, '{}' as it's not a valid GraphQL post data.");
+ }
+ } else if (HTTPConstants.GET.equals(method)) {
+ try {
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams(sampler.getArguments(),
+ sampler.getContentEncoding());
+ } catch (Exception e) {
+ log.debug("Ignoring request, '{}' as it does not valid GraphQL arguments.");
+ }
+ }
+
+ if (params != null) {
+ sampler.setProperty(TestElement.GUI_CLASS, GraphQLHTTPSamplerGui.class.getName());
+ sampler.setProperty(GraphQLUrlConfigGui.OPERATION_NAME, params.getOperationName());
+ sampler.setProperty(GraphQLUrlConfigGui.QUERY, params.getQuery());
+ sampler.setProperty(GraphQLUrlConfigGui.VARIABLES, params.getVariables());
+ }
}
/**
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/HttpRequestHdr.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/HttpRequestHdr.java
index 961627b..abd69a8 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/HttpRequestHdr.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/HttpRequestHdr.java
@@ -86,6 +86,8 @@ public class HttpRequestHdr {
private String httpSampleNameFormat;
+ private boolean detectGraphQLRequest;
+
public HttpRequestHdr() {
this("", "");
}
@@ -120,6 +122,22 @@ public class HttpRequestHdr {
}
/**
+ * Return true if automatic GraphQL Request detection is enabled.
+ * @return true if automatic GraphQL Request detection is enabled
+ */
+ public boolean isDetectGraphQLRequest() {
+ return detectGraphQLRequest;
+ }
+
+ /**
+ * Sets whether automatic GraphQL Request detection is enabled.
+ * @param detectGraphQLRequest whether automatic GraphQL Request detection is enabled
+ */
+ public void setDetectGraphQLRequest(boolean detectGraphQLRequest) {
+ this.detectGraphQLRequest = detectGraphQLRequest;
+ }
+
+ /**
* Parses a http header from a stream.
*
* @param in
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/Proxy.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/Proxy.java
index 5faf29b..37c1f47 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/Proxy.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/Proxy.java
@@ -163,6 +163,8 @@ public class Proxy extends Thread {
HttpRequestHdr request = new HttpRequestHdr(target.getPrefixHTTPSampleName(), httpSamplerName,
target.getHTTPSampleNamingMode(), target.getHttpSampleNameFormat());
+ request.setDetectGraphQLRequest(target.getDetectGraphQLRequest());
+
SampleResult result = null;
HeaderManager headers = null;
HTTPSamplerBase sampler = null;
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/ProxyControl.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/ProxyControl.java
index 4341da2..642b943 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/ProxyControl.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/ProxyControl.java
@@ -136,6 +136,7 @@ public class ProxyControl extends GenericController implements NonTestElement {
private static final String SAMPLER_REDIRECT_AUTOMATICALLY = "ProxyControlGui.sampler_redirect_automatically"; // $NON-NLS-1$
private static final String SAMPLER_FOLLOW_REDIRECTS = "ProxyControlGui.sampler_follow_redirects"; // $NON-NLS-1$
private static final String USE_KEEPALIVE = "ProxyControlGui.use_keepalive"; // $NON-NLS-1$
+ private static final String DETECT_GRAPHQL_REQUEST = "ProxyControlGui.detect_graphql_request"; // $NON-NLS-1$
private static final String SAMPLER_DOWNLOAD_IMAGES = "ProxyControlGui.sampler_download_images"; // $NON-NLS-1$
private static final String HTTP_SAMPLER_NAMING_MODE = "ProxyControlGui.proxy_http_sampler_naming_mode"; // $NON-NLS-1$
private static final String HTTP_SAMPLER_FORMAT = "ProxyControlGui.proxy_http_sampler_format"; // $NON-NLS-1$
@@ -268,6 +269,8 @@ public class ProxyControl extends GenericController implements NonTestElement {
private volatile boolean regexMatch = false;
+ private volatile boolean detectGraphQLRequest = false;
+
private Set<Class<?>> addableInterfaces = new HashSet<>(
Arrays.asList(Visualizer.class, ConfigElement.class,
Assertion.class, Timer.class, PreProcessor.class,
@@ -360,6 +363,11 @@ public class ProxyControl extends GenericController implements NonTestElement {
setProperty(new BooleanProperty(USE_KEEPALIVE, b));
}
+ public void setDetectGraphQLRequest(boolean b) {
+ detectGraphQLRequest = b;
+ setProperty(new BooleanProperty(DETECT_GRAPHQL_REQUEST, b));
+ }
+
public void setSamplerDownloadImages(boolean b) {
samplerDownloadImages = b;
setProperty(new BooleanProperty(SAMPLER_DOWNLOAD_IMAGES, b));
@@ -460,6 +468,10 @@ public class ProxyControl extends GenericController implements NonTestElement {
return getPropertyAsBoolean(USE_KEEPALIVE, true);
}
+ public boolean getDetectGraphQLRequest() {
+ return getPropertyAsBoolean(DETECT_GRAPHQL_REQUEST, true);
+ }
+
public boolean getSamplerDownloadImages() {
return getPropertyAsBoolean(SAMPLER_DOWNLOAD_IMAGES, false);
}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java
index a01dd58..d79428c 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/proxy/gui/ProxyControlGui.java
@@ -148,6 +148,11 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
*/
private JCheckBox useKeepAlive;
+ /**
+ * Set/clear the Detect GraphQL Request box on the samplers (default is true)
+ */
+ private JCheckBox detectGraphQLRequest;
+
/*
* Use regexes to match the source data
*/
@@ -324,6 +329,7 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
model.setSamplerRedirectAutomatically(samplerRedirectAutomatically.isSelected());
model.setSamplerFollowRedirects(samplerFollowRedirects.isSelected());
model.setUseKeepAlive(useKeepAlive.isSelected());
+ model.setDetectGraphQLRequest(detectGraphQLRequest.isSelected());
model.setSamplerDownloadImages(samplerDownloadImages.isSelected());
model.setHTTPSampleNamingMode(httpSampleNamingMode.getSelectedIndex());
model.setDefaultEncoding(defaultEncoding.getText());
@@ -392,6 +398,7 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
samplerRedirectAutomatically.setSelected(model.getSamplerRedirectAutomatically());
samplerFollowRedirects.setSelected(model.getSamplerFollowRedirects());
useKeepAlive.setSelected(model.getUseKeepalive());
+ detectGraphQLRequest.setSelected(model.getDetectGraphQLRequest());
samplerDownloadImages.setSelected(model.getSamplerDownloadImages());
httpSampleNamingMode.setSelectedIndex(model.getHTTPSampleNamingMode());
prefixHTTPSampleName.setText(model.getPrefixHTTPSampleName());
@@ -757,6 +764,7 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
testPlanPanel.add(createTestPlanContentPanel());
testPlanPanel.add(Box.createVerticalStrut(5));
testPlanPanel.add(createHTTPSamplerPanel());
+ testPlanPanel.add(createGraphQLHTTPSamplerPanel());
tabbedPane.add(JMeterUtils
.getResString("proxy_test_plan_creation"), testPlanPanel);
@@ -993,6 +1001,20 @@ public class ProxyControlGui extends LogicControllerGui implements JMeterGUIComp
panel.add(labelSamplerType);
panel.add(samplerTypeName, "growx, span");
+
+ return panel;
+ }
+
+ private JPanel createGraphQLHTTPSamplerPanel() {
+ detectGraphQLRequest = new JCheckBox(JMeterUtils.getResString("detect_graphql_request")); // $NON-NLS-1$
+ detectGraphQLRequest.setSelected(true);
+ detectGraphQLRequest.addActionListener(this);
+ detectGraphQLRequest.setActionCommand(ENABLE_RESTART);
+
+ JPanel panel = new JPanel(new MigLayout("fillx, wrap 3"));
+ panel.setBorder(BorderFactory.createTitledBorder(JMeterUtils.getResString("proxy_sampler_graphql_settings"))); // $NON-NLS-1$
+ panel.add(detectGraphQLRequest);
+
return panel;
}
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/util/GraphQLRequestParamUtils.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/util/GraphQLRequestParamUtils.java
new file mode 100644
index 0000000..6844722
--- /dev/null
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/util/GraphQLRequestParamUtils.java
@@ -0,0 +1,254 @@
+/*
+ * 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.protocol.http.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.RegExUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.entity.ContentType;
+import org.apache.jmeter.config.Argument;
+import org.apache.jmeter.config.Arguments;
+import org.apache.jmeter.protocol.http.config.GraphQLRequestParams;
+import org.apache.jmeter.testelement.property.JMeterProperty;
+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.JsonNodeType;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+/**
+ * Utilities to (de)serialize GraphQL request parameters.
+ */
+public final class GraphQLRequestParamUtils {
+
+ private static Logger log = LoggerFactory.getLogger(GraphQLRequestParamUtils.class);
+
+ private static Pattern WHITESPACES_PATTERN = Pattern.compile("\\p{Space}+");
+
+ private GraphQLRequestParamUtils() {
+ }
+
+ /**
+ * Return true if the content type is GraphQL content type (i.e. 'application/json').
+ * @param contentType Content-Type value
+ * @return true if the content type is GraphQL content type
+ */
+ public static boolean isGraphQLContentType(final String contentType) {
+ if (StringUtils.isEmpty(contentType)) {
+ return false;
+ }
+ final ContentType type = ContentType.parse(contentType);
+ return ContentType.APPLICATION_JSON.getMimeType().equals(type.getMimeType());
+ }
+
+ /**
+ * Convert the GraphQL request parameters input data to an HTTP POST body string.
+ * @param params GraphQL request parameter input data
+ * @return an HTTP POST body string converted from the GraphQL request parameters input data
+ * @throws RuntimeException if JSON serialization fails for some reason due to any runtime environment issues
+ */
+ public static String toPostBodyString(final GraphQLRequestParams params) {
+ final ObjectMapper mapper = new ObjectMapper();
+ final ObjectNode postBodyJson = mapper.createObjectNode();
+ postBodyJson.put("operationName", StringUtils.trimToNull(params.getOperationName()));
+
+ if (StringUtils.isNotBlank(params.getVariables())) {
+ try {
+ final ObjectNode variablesJson = mapper.readValue(params.getVariables(), ObjectNode.class);
+ postBodyJson.set("variables", variablesJson);
+ } catch (JsonProcessingException e) {
+ log.error("Ignoring the GraphQL query variables content due to the syntax error: {}",
+ e.getLocalizedMessage());
+ }
+ }
+
+ postBodyJson.put("query", StringUtils.trim(params.getQuery()));
+
+ try {
+ return mapper.writeValueAsString(postBodyJson);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Cannot serialize JSON for POST body string", e);
+ }
+ }
+
+ /**
+ * Convert the GraphQL Query input string into an HTTP GET request parameter value.
+ * @param query the GraphQL Query input string
+ * @return an HTTP GET request parameter value converted from the GraphQL Query input string
+ */
+ public static String queryToGetParamValue(final String query) {
+ return RegExUtils.replaceAll(StringUtils.trim(query), WHITESPACES_PATTERN, " ");
+ }
+
+ /**
+ * Convert the GraphQL Variables JSON input string into an HTTP GET request parameter value.
+ * @param variables the GraphQL Variables JSON input string
+ * @return an HTTP GET request parameter value converted from the GraphQL Variables JSON input string
+ */
+ public static String variablesToGetParamValue(final String variables) {
+ final ObjectMapper mapper = new ObjectMapper();
+
+ try {
+ final ObjectNode variablesJson = mapper.readValue(variables, ObjectNode.class);
+ return mapper.writeValueAsString(variablesJson);
+ } catch (JsonProcessingException e) {
+ log.error("Ignoring the GraphQL query variables content due to the syntax error: {}",
+ e.getLocalizedMessage());
+ }
+
+ return null;
+ }
+
+ /**
+ * Parse {@code postData} and convert it to a {@link GraphQLRequestParams} object if it is a valid GraphQL post data.
+ * @param postData post data
+ * @param contentEncoding content encoding
+ * @return a converted {@link GraphQLRequestParams} object form the {@code postData}
+ * @throws IllegalArgumentException if {@code postData} is not a GraphQL post JSON data or not a valid JSON
+ * @throws JsonProcessingException if it fails to serialize a parsed JSON object to string
+ * @throws UnsupportedEncodingException if it fails to decode parameter value
+ */
+ public static GraphQLRequestParams toGraphQLRequestParams(byte[] postData, final String contentEncoding)
+ throws IllegalArgumentException, JsonProcessingException, UnsupportedEncodingException {
+ final String encoding = StringUtils.isNotEmpty(contentEncoding) ? contentEncoding
+ : EncoderCache.URL_ARGUMENT_ENCODING;
+
+ final ObjectMapper mapper = new ObjectMapper();
+ ObjectNode data;
+
+ try (InputStreamReader reader = new InputStreamReader(new ByteArrayInputStream(postData), encoding)) {
+ data = mapper.readValue(reader, ObjectNode.class);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Invalid json data: " + e.getLocalizedMessage());
+ }
+
+ String operationName = null;
+ String query = null;
+ String variables = null;
+
+ final JsonNode operationNameNode = data.has("operationName") ? data.get("operationName") : null;
+ if (operationNameNode != null) {
+ operationName = getJsonNodeTextContent(operationNameNode, true);
+ }
+
+ if (!data.has("query")) {
+ throw new IllegalArgumentException("Not a valid GraphQL query.");
+ }
+ final JsonNode queryNode = data.get("query");
+ query = getJsonNodeTextContent(queryNode, false);
+ final String trimmedQuery = StringUtils.trim(query);
+ if (!StringUtils.startsWith(trimmedQuery, "query") && !StringUtils.startsWith(trimmedQuery, "mutation")) {
+ throw new IllegalArgumentException("Not a valid GraphQL query.");
+ }
+
+ final JsonNode variablesNode = data.has("variables") ? data.get("variables") : null;
+ if (variablesNode != null) {
+ final JsonNodeType nodeType = variablesNode.getNodeType();
+ if (nodeType != JsonNodeType.NULL) {
+ if (nodeType == JsonNodeType.OBJECT) {
+ variables = mapper.writeValueAsString(variablesNode);
+ } else {
+ throw new IllegalArgumentException("Not a valid object node for GraphQL variables.");
+ }
+ }
+ }
+
+ return new GraphQLRequestParams(operationName, query, variables);
+ }
+
+ /**
+ * Parse {@code arguments} and convert it to a {@link GraphQLRequestParams} object if it has valid GraphQL HTTP arguments.
+ * @param arguments arguments
+ * @param contentEncoding content encoding
+ * @return a converted {@link GraphQLRequestParams} object form the {@code arguments}
+ * @throws IllegalArgumentException if {@code arguments} does not contain valid GraphQL request arguments
+ * @throws UnsupportedEncodingException if it fails to decode parameter value
+ */
+ public static GraphQLRequestParams toGraphQLRequestParams(final Arguments arguments, final String contentEncoding)
+ throws IllegalArgumentException, UnsupportedEncodingException {
+ final String encoding = StringUtils.isNotEmpty(contentEncoding) ? contentEncoding
+ : EncoderCache.URL_ARGUMENT_ENCODING;
+
+ String operationName = null;
+ String query = null;
+ String variables = null;
+
+ for (JMeterProperty prop : arguments) {
+ final Argument arg = (Argument) prop.getObjectValue();
+ if (!(arg instanceof HTTPArgument)) {
+ continue;
+ }
+
+ final String name = arg.getName();
+ final String metadata = arg.getMetaData();
+ final String value = StringUtils.trimToNull(arg.getValue());
+
+ if ("=".equals(metadata) && value != null) {
+ final boolean alwaysEncoded = ((HTTPArgument) arg).isAlwaysEncoded();
+
+ if ("operationName".equals(name)) {
+ operationName = alwaysEncoded ? value : URLDecoder.decode(value, encoding);
+ } else if ("query".equals(name)) {
+ query = alwaysEncoded ? value : URLDecoder.decode(value, encoding);
+ } else if ("variables".equals(name)) {
+ variables = alwaysEncoded ? value : URLDecoder.decode(value, encoding);
+ }
+ }
+ }
+
+ if (StringUtils.isEmpty(query)
+ || (!StringUtils.startsWith(query, "query") && !StringUtils.startsWith(query, "mutation"))) {
+ throw new IllegalArgumentException("Not a valid GraphQL query.");
+ }
+
+ if (StringUtils.isNotEmpty(variables)) {
+ if (!StringUtils.startsWith(variables, "{") || !StringUtils.endsWith(variables, "}")) {
+ throw new IllegalArgumentException("Not a valid object node for GraphQL variables.");
+ }
+ }
+
+ return new GraphQLRequestParams(operationName, query, variables);
+ }
+
+ private static String getJsonNodeTextContent(final JsonNode jsonNode, final boolean nullable) throws IllegalArgumentException {
+ final JsonNodeType nodeType = jsonNode.getNodeType();
+
+ if (nodeType == JsonNodeType.NULL) {
+ if (nullable) {
+ return null;
+ }
+
+ throw new IllegalArgumentException("Not a non-null value node.");
+ }
+
+ if (nodeType == JsonNodeType.STRING) {
+ return jsonNode.asText();
+ }
+
+ throw new IllegalArgumentException("Not a string value node.");
+ }
+}
diff --git a/src/protocol/http/src/main/resources/org/apache/jmeter/protocol/http/sampler/GraphQLHTTPSamplerResources.properties b/src/protocol/http/src/main/resources/org/apache/jmeter/protocol/http/sampler/GraphQLHTTPSamplerResources.properties
new file mode 100644
index 0000000..27951e8
--- /dev/null
+++ b/src/protocol/http/src/main/resources/org/apache/jmeter/protocol/http/sampler/GraphQLHTTPSamplerResources.properties
@@ -0,0 +1,27 @@
+#
+# 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.
+#
+
+displayName=GraphQL HTTP Request
+defaults.displayName=Default Test Values
+method.displayName=Method
+method.shortDescription=Method
+operationName.displayName=Operation Name
+operationName.shortDescription=Operation Name
+query.displayName=Query
+query.shortDescription=Query or Mutation
+variables.displayName=Variables
+variables.shortDescription=Variables
diff --git a/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/util/TestGraphQLRequestParamUtils.java b/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/util/TestGraphQLRequestParamUtils.java
new file mode 100644
index 0000000..8316e68
--- /dev/null
+++ b/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/util/TestGraphQLRequestParamUtils.java
@@ -0,0 +1,212 @@
+/*
+ * 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.protocol.http.util;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.jmeter.config.Arguments;
+import org.apache.jmeter.protocol.http.config.GraphQLRequestParams;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.github.jknack.handlebars.internal.lang3.StringUtils;
+
+public class TestGraphQLRequestParamUtils {
+
+ private static final String OPERATION_NAME = "";
+
+ private static final String QUERY =
+ "query($id: ID!) {\n"
+ + " droid(id: $id) {\n"
+ + " id\n"
+ + " name\n"
+ + " friends {\n"
+ + " id\n"
+ + " name\n"
+ + " appearsIn\n"
+ + " }\n"
+ + " }\n"
+ + "}\n";
+
+ private static final String VARIABLES =
+ "{\n"
+ + " \"id\": \"2001\"\n"
+ + "}\n";
+
+ private static final String EXPECTED_QUERY_GET_PARAM_VALUE =
+ "query($id: ID!) { droid(id: $id) { id name friends { id name appearsIn } } }";
+
+ private static final String EXPECTED_VARIABLES_GET_PARAM_VALUE = "{\"id\":\"2001\"}";
+
+ private static final String EXPECTED_POST_BODY =
+ "{"
+ + "\"operationName\":null,"
+ + "\"variables\":" + EXPECTED_VARIABLES_GET_PARAM_VALUE + ","
+ + "\"query\":\"" + StringUtils.replace(QUERY.trim(), "\n", "\\n") + "\""
+ + "}";
+
+ private GraphQLRequestParams params;
+
+ @BeforeEach
+ public void setUp() {
+ params = new GraphQLRequestParams(OPERATION_NAME, QUERY, VARIABLES);
+ }
+
+ @Test
+ public void testIsGraphQLContentType() throws Exception {
+ assertTrue(GraphQLRequestParamUtils.isGraphQLContentType("application/json"));
+ assertTrue(GraphQLRequestParamUtils.isGraphQLContentType("application/json;charset=utf-8"));
+ assertTrue(GraphQLRequestParamUtils.isGraphQLContentType("application/json; charset=utf-8"));
+
+ assertFalse(GraphQLRequestParamUtils.isGraphQLContentType("application/vnd.api+json"));
+ assertFalse(GraphQLRequestParamUtils.isGraphQLContentType("application/json-patch+json"));
+ assertFalse(GraphQLRequestParamUtils.isGraphQLContentType(""));
+ assertFalse(GraphQLRequestParamUtils.isGraphQLContentType(null));
+ }
+
+ @Test
+ public void testToPostBodyString() throws Exception {
+ assertEquals(EXPECTED_POST_BODY, GraphQLRequestParamUtils.toPostBodyString(params));
+ }
+
+ @Test
+ public void testQueryToGetParamValue() throws Exception {
+ assertEquals(EXPECTED_QUERY_GET_PARAM_VALUE, GraphQLRequestParamUtils.queryToGetParamValue(params.getQuery()));
+ }
+
+ @Test
+ public void testVariablesToGetParamValue() throws Exception {
+ assertEquals(EXPECTED_VARIABLES_GET_PARAM_VALUE,
+ GraphQLRequestParamUtils.variablesToGetParamValue(params.getVariables()));
+ }
+
+ @Test
+ public void testToGraphQLRequestParamsWithPostData() throws Exception {
+ GraphQLRequestParams params = GraphQLRequestParamUtils
+ .toGraphQLRequestParams(EXPECTED_POST_BODY.getBytes(StandardCharsets.UTF_8), null);
+ assertNull(params.getOperationName());
+ assertEquals(QUERY.trim(), params.getQuery());
+ assertEquals(EXPECTED_VARIABLES_GET_PARAM_VALUE, params.getVariables());
+
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams(
+ "{\"operationName\":\"op1\",\"variables\":{\"id\":123},\"query\":\"query { droid { id }}\"}"
+ .getBytes(StandardCharsets.UTF_8),
+ null);
+ assertEquals("op1", params.getOperationName());
+ assertEquals("query { droid { id }}", params.getQuery());
+ assertEquals("{\"id\":123}", params.getVariables());
+
+ try {
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams("".getBytes(StandardCharsets.UTF_8), null);
+ fail("Should have failed due to invalid json data.");
+ } catch (IllegalArgumentException ignore) {
+ }
+
+ try {
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams("{}".getBytes(StandardCharsets.UTF_8), null);
+ fail("Should have failed due to invalid json data.");
+ } catch (IllegalArgumentException ignore) {
+ }
+
+ try {
+ params = GraphQLRequestParamUtils
+ .toGraphQLRequestParams("{\"query\":\"select * from emp\"}".getBytes(StandardCharsets.UTF_8), null);
+ fail("Should have failed due to invalid graph query param.");
+ } catch (IllegalArgumentException ignore) {
+ }
+
+ try {
+ params = GraphQLRequestParamUtils
+ .toGraphQLRequestParams("{\"operationName\":{\"id\":123},\"query\":\"query { droid { id }}\"}"
+ .getBytes(StandardCharsets.UTF_8), null);
+ fail("Should have failed due to invalid graph operationName type.");
+ } catch (IllegalArgumentException ignore) {
+ }
+
+ try {
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams(
+ "{\"variables\":\"r2d2\",\"query\":\"query { droid { id }}\"}".getBytes(StandardCharsets.UTF_8),
+ null);
+ fail("Should have failed due to invalid graph variables type.");
+ } catch (IllegalArgumentException ignore) {
+ }
+ }
+
+ @Test
+ public void testToGraphQLRequestParamsWithHttpArguments() throws Exception {
+ Arguments args = new Arguments();
+ args.addArgument(new HTTPArgument("query", "query { droid { id }}", "=", false));
+ GraphQLRequestParams params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+ assertNull(params.getOperationName());
+ assertEquals("query { droid { id }}", params.getQuery());
+ assertNull(params.getVariables());
+
+ args = new Arguments();
+ args.addArgument(new HTTPArgument("operationName", "op1", "=", false));
+ args.addArgument(new HTTPArgument("query", "query { droid { id }}", "=", false));
+ args.addArgument(new HTTPArgument("variables", "{\"id\":123}", "=", false));
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+ assertEquals("op1", params.getOperationName());
+ assertEquals("query { droid { id }}", params.getQuery());
+ assertEquals("{\"id\":123}", params.getVariables());
+
+ args = new Arguments();
+ args.addArgument(new HTTPArgument("query", "query+%7B+droid+%7B+id+%7D%7D", "=", true));
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+ assertNull(params.getOperationName());
+ assertEquals("query { droid { id }}", params.getQuery());
+ assertNull(params.getVariables());
+
+ args = new Arguments();
+ args.addArgument(new HTTPArgument("query", "query%20%7B%20droid%20%7B%20id%20%7D%7D", "=", true));
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+ assertNull(params.getOperationName());
+ assertEquals("query { droid { id }}", params.getQuery());
+ assertNull(params.getVariables());
+
+ try {
+ args = new Arguments();
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+ fail("Should have failed due to missing GraphQL parameters.");
+ } catch (IllegalArgumentException ignore) {
+ }
+
+ try {
+ args = new Arguments();
+ args.addArgument(new HTTPArgument("query", "select * from emp", "=", false));
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+ fail("Should have failed due to invalid graph query param.");
+ } catch (IllegalArgumentException ignore) {
+ }
+
+ try {
+ args = new Arguments();
+ args.addArgument(new HTTPArgument("query", "query { droid { id }}", "=", false));
+ args.addArgument(new HTTPArgument("variables", "r2d2", "=", false));
+ params = GraphQLRequestParamUtils.toGraphQLRequestParams(args, null);
+ fail("Should have failed due to invalid graph query param.");
+ } catch (IllegalArgumentException ignore) {
+ }
+ }
+}
diff --git a/xdocs/changes.xml b/xdocs/changes.xml
index 4fb8339..d6a24f4 100644
--- a/xdocs/changes.xml
+++ b/xdocs/changes.xml
@@ -83,6 +83,7 @@ applications when JMeter is starting up.</p>
<ul>
<li><bug>53848</bug><bug>63527</bug>Implement a new setting to allow the exclusion of embedded URLs</li>
<li><bug>64696</bug><pr>571</pr><pr>595</pr>Freestyle format for names in (Default)SamplerCreater. Based on a patch by Vincent Daburon (vdaburon at gmail.com)</li>
+ <li><bug>64752</bug>Add GraphQL/HTTP Request Sampler. Contributed by woonsan.</li>
</ul>
<h3>Other samplers</h3>
diff --git a/xdocs/demos/SimpleGraphQLTestPlan.jmx b/xdocs/demos/SimpleGraphQLTestPlan.jmx
new file mode 100644
index 0000000..7d3d49f
--- /dev/null
+++ b/xdocs/demos/SimpleGraphQLTestPlan.jmx
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.3.1-SNAPSHOT 790e46c">
+ <hashTree>
+ <TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="Simple GraphQL HTTP Request Test Plan" enabled="true">
+ <stringProp name="TestPlan.comments">Simple GraphQL HTTP Request Test Plan for demonstration purpose, querying data from a demo GraphQL server</stringProp>
+ <boolProp name="TestPlan.functional_mode">false</boolProp>
+ <boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
+ <boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
+ <elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+ <collectionProp name="Arguments.arguments"/>
+ </elementProp>
+ <stringProp name="TestPlan.user_define_classpath"></stringProp>
+ </TestPlan>
+ <hashTree>
+ <ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP Request Defaults" enabled="true">
+ <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+ <collectionProp name="Arguments.arguments"/>
+ </elementProp>
+ <stringProp name="HTTPSampler.domain">localhost</stringProp>
+ <stringProp name="HTTPSampler.port">8080</stringProp>
+ <stringProp name="HTTPSampler.protocol">http</stringProp>
+ <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+ <stringProp name="HTTPSampler.path"></stringProp>
+ <stringProp name="HTTPSampler.concurrentPool">6</stringProp>
+ <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+ <stringProp name="HTTPSampler.response_timeout"></stringProp>
+ </ConfigTestElement>
+ <hashTree/>
+ <HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="HTTP Header Manager" enabled="true">
+ <collectionProp name="HeaderManager.headers">
+ <elementProp name="" elementType="Header">
+ <stringProp name="Header.name">Content-Type</stringProp>
+ <stringProp name="Header.value">application/json</stringProp>
+ </elementProp>
+ </collectionProp>
+ </HeaderManager>
+ <hashTree/>
+ <ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="Thread Group" enabled="true">
+ <stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
+ <elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="Loop Controller" enabled="true">
+ <boolProp name="LoopController.continue_forever">false</boolProp>
+ <stringProp name="LoopController.loops">1</stringProp>
+ </elementProp>
+ <stringProp name="ThreadGroup.num_threads">1</stringProp>
+ <stringProp name="ThreadGroup.ramp_time">1</stringProp>
+ <boolProp name="ThreadGroup.scheduler">false</boolProp>
+ <stringProp name="ThreadGroup.duration"></stringProp>
+ <stringProp name="ThreadGroup.delay"></stringProp>
+ <boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
+ </ThreadGroup>
+ <hashTree>
+ <HTTPSamplerProxy guiclass="GraphQLHTTPSamplerGui" testclass="HTTPSamplerProxy" testname="GraphQL HTTP Request to get the favorite droid" enabled="true">
+ <elementProp name="HTTPsampler.Arguments" elementType="Arguments">
+ <collectionProp name="Arguments.arguments">
+ <elementProp name="" elementType="HTTPArgument" enabled="true">
+ <boolProp name="HTTPArgument.always_encode">false</boolProp>
+ <stringProp name="Argument.value">{"operationName":null,"variables":{"id":"2001"},"query":"query($id: ID!) {\n droid(id: $id) {\n id\n name\n friends {\n id\n name\n appearsIn\n }\n }\n}"}</stringProp>
+ <stringProp name="Argument.metadata">=</stringProp>
+ <boolProp name="HTTPArgument.use_equals">true</boolProp>
+ </elementProp>
+ </collectionProp>
+ </elementProp>
+ <stringProp name="HTTPSampler.domain"></stringProp>
+ <stringProp name="HTTPSampler.port"></stringProp>
+ <stringProp name="HTTPSampler.protocol"></stringProp>
+ <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+ <stringProp name="HTTPSampler.path">/graphql</stringProp>
+ <stringProp name="HTTPSampler.method">POST</stringProp>
+ <boolProp name="HTTPSampler.follow_redirects">false</boolProp>
+ <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+ <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+ <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+ <stringProp name="GraphQLHTTPSampler.operationName"></stringProp>
+ <stringProp name="GraphQLHTTPSampler.query">query($id: ID!) {
+ droid(id: $id) {
+ id
+ name
+ friends {
+ id
+ name
+ appearsIn
+ }
+ }
+}</stringProp>
+ <stringProp name="GraphQLHTTPSampler.variables">{
+ "id": "2001"
+}</stringProp>
+ <boolProp name="HTTPSampler.postBodyRaw">true</boolProp>
+ <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+ <stringProp name="HTTPSampler.embedded_url_exclude_re"></stringProp>
+ <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+ <stringProp name="HTTPSampler.response_timeout"></stringProp>
+ </HTTPSamplerProxy>
+ <hashTree/>
+ </hashTree>
+ <ResultCollector guiclass="ViewResultsFullVisualizer" testclass="ResultCollector" testname="View Results Tree" enabled="true">
+ <boolProp name="ResultCollector.error_logging">false</boolProp>
+ <objProp>
+ <name>saveConfig</name>
+ <value class="SampleSaveConfiguration">
+ <time>true</time>
+ <latency>true</latency>
+ <timestamp>true</timestamp>
+ <success>true</success>
+ <label>true</label>
+ <code>true</code>
+ <message>true</message>
+ <threadName>true</threadName>
+ <dataType>true</dataType>
+ <encoding>false</encoding>
+ <assertions>true</assertions>
+ <subresults>true</subresults>
+ <responseData>false</responseData>
+ <samplerData>false</samplerData>
+ <xml>false</xml>
+ <fieldNames>true</fieldNames>
+ <responseHeaders>false</responseHeaders>
+ <requestHeaders>false</requestHeaders>
+ <responseDataOnError>false</responseDataOnError>
+ <saveAssertionResultsFailureMessage>true</saveAssertionResultsFailureMessage>
+ <assertionsResultsToSave>0</assertionsResultsToSave>
+ <bytes>true</bytes>
+ <sentBytes>true</sentBytes>
+ <url>true</url>
+ <threadCounts>true</threadCounts>
+ <idleTime>true</idleTime>
+ <connectTime>true</connectTime>
+ </value>
+ </objProp>
+ <stringProp name="filename"></stringProp>
+ </ResultCollector>
+ <hashTree/>
+ </hashTree>
+ </hashTree>
+</jmeterTestPlan>
diff --git a/xdocs/images/screenshots/graphql-http-request-vars.png b/xdocs/images/screenshots/graphql-http-request-vars.png
new file mode 100644
index 0000000..b6e0d9e
Binary files /dev/null and b/xdocs/images/screenshots/graphql-http-request-vars.png differ
diff --git a/xdocs/images/screenshots/graphql-http-request.png b/xdocs/images/screenshots/graphql-http-request.png
new file mode 100644
index 0000000..50a3609
Binary files /dev/null and b/xdocs/images/screenshots/graphql-http-request.png differ
diff --git a/xdocs/usermanual/component_reference.xml b/xdocs/usermanual/component_reference.xml
index 9e8cf07..b241ac5 100644
--- a/xdocs/usermanual/component_reference.xml
+++ b/xdocs/usermanual/component_reference.xml
@@ -130,7 +130,7 @@ Latency is set to the time it takes to login.
them. This can save you time if you have a lot of HTTP requests or requests with many
parameters.</p>
- <p><b>There are two different test elements used to define the samplers:</b></p>
+ <p><b>There are three different test elements used to define the samplers:</b></p>
<dl>
<dt>AJP/1.3 Sampler</dt><dd>uses the Tomcat mod_jk protocol (allows testing of Tomcat in AJP mode without needing Apache httpd)
The AJP Sampler does not support multiple file upload; only the first file will be used.
@@ -143,6 +143,17 @@ Latency is set to the time it takes to login.
<dt>Blank Value</dt><dd>does not set implementation on HTTP Samplers, so relies on HTTP Request Defaults if present or on <code>jmeter.httpsampler</code> property defined in <code>jmeter.properties</code></dd>
</dl>
</dd>
+ <dt>GraphQL HTTP Request</dt><dd>this is a GUI variation of the <b>HTTP Request</b> to provide more convenient UI elements
+ to view or edit GraphQL <b>Query</b>, <b>Variables</b> and <b>Operation Name</b>, while converting them into HTTP Arguments automatically under the hood
+ using the same sampler.
+ This hides or customizes the following UI elements as they are less convenient for or irrelevant to GraphQL over HTTP/HTTPS requests:
+ <ul>
+ <li><b>Method</b>: Only POST and GET methods are available conforming the GraphQL over HTTP specification. POST method is selected by default.</li>
+ <li><b>Parameters</b> and <b>Post Body</b> tabs: you may view or edit parameter content through Query, Variables and Operation Name UI elements instead.</li>
+ <li><b>File Upload</b> tab: irrelevant to GraphQL queries.</li>
+ <li><b>Embedded Resources from HTML Files</b> section in the Advanced tab: irrelevant in GraphQL JSON responses.</li>
+ </ul>
+ </dd>
</dl>
<p>The Java HTTP implementation has some limitations:</p>
<ul>
@@ -201,6 +212,8 @@ https.default.protocol=SSLv3
for additional configuration steps.</p>
</description>
<figure width="951" height="284" image="http-request-advanced-tab.png">HTTP Request Advanced config fields</figure>
+<figure width="950" height="618" image="graphql-http-request.png">Screenshot of Control-Panel of GraphQL HTTP Request</figure>
+<figure width="950" height="618" image="graphql-http-request-vars.png">Variables field for GraphQL HTTP Request</figure>
<properties>
<property name="Name" required="No">Descriptive name for this sampler that is shown in the tree.</property>
@@ -363,6 +376,19 @@ and send HTTP/HTTPS requests for all images, Java applets, JavaScript files, CSS
If the property <code>httpclient.localaddress</code> is defined, that is used for all HttpClient requests.
</property>
</properties>
+ <p>The following parameters are available only for <b>GraphQL HTTP Request</b>:</p>
+ <properties>
+ <property name="Query" required="Yes">
+ GraphQL query (or mutation) statement.
+ </property>
+ <property name="Variables" required="No">
+ GraphQL query (or mutation) variables in a valid JSON string.
+ <b>Note</b>: If the input string is not a valid JSON string, this will be ignored with an ERROR log.
+ </property>
+ <property name="Operation Name" required="No">
+ Optional GraphQL operation name when making a request for multi-operation documents.
+ </property>
+ </properties>
<note>
When using Automatic Redirection, cookies are only sent for the initial URL.
This can cause unexpected behaviour for web-sites that redirect to a local server.
|