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/10/04 05:32:36 UTC
[jmeter] branch master updated: Add Bolt protocol support for Neo4j
database (#510)
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 2cfdec9 Add Bolt protocol support for Neo4j database (#510)
2cfdec9 is described below
commit 2cfdec9d50ddab4649b813c07724588a03371ef4
Author: Nicolas Mervaillie <nm...@users.noreply.github.com>
AuthorDate: Fri Oct 4 07:32:30 2019 +0200
Add Bolt protocol support for Neo4j database (#510)
* Add Bolt protocol support
Add driver dependency
Add bolt config element and sampler
Expose cypher query parameter, execute cypher
Add temporary README on how to build for hackathon submission
* Add default example value for parameters to better guide users
* Rename configuration classes for clarity
* Apply remarks from code review and code cleanup
* Use TextArea input for query and param fields
Simple text inputs are too small
* Add bolt elements to SaveService
* Add documentation for bolt protocol
* Fix build scripts
Fix some dependencies and add the bolt protocol to the dist build
* Remove README-bolt.md to submit PR
* Fix failing test
build says: SaveService nameMap (saveservice.properties) should contain org.apache.jmeter.protocol.bolt.sampler.AbstractBoltTestElement
* Add some unit tests
* Use single line logging instead of multi line
* Avoid using lambda when consuming results to avoid performance hit
* Add documentation about connection pooling and what's included in response time
* Add neo4j driver trust-key
This resolves Bug 63801 - Add Bolt protocol support for Neo4j database
https://bz.apache.org/bugzilla/show_bug.cgi?id=63801
---
bin/saveservice.properties | 2 +
checksum.xml | 1 +
gradle.properties | 1 +
settings.gradle.kts | 1 +
src/bom/build.gradle.kts | 1 +
.../java/org/apache/jmeter/save/SaveService.java | 2 +-
src/dist/build.gradle.kts | 1 +
.../bolt/config/BoltConnectionElement.java | 121 ++++++++++++++
.../bolt/config/BoltConnectionElementBeanInfo.java | 57 +++++++
.../bolt/sampler/AbstractBoltTestElement.java | 52 ++++++
.../jmeter/protocol/bolt/sampler/BoltSampler.java | 175 +++++++++++++++++++++
.../protocol/bolt/sampler/BoltSamplerBeanInfo.java | 26 +++
.../sampler/BoltTestElementBeanInfoSupport.java | 50 ++++++
.../BoltConnectionElementResources.properties | 26 +++
.../bolt/sampler/BoltSamplerResources.properties | 28 ++++
.../protocol/bolt/sampler/BoltSamplerSpec.groovy | 123 +++++++++++++++
.../jmeter/resources/ResourceKeyUsageTestBolt.java | 23 +++
src/protocol/build.gradle.kts | 9 ++
.../images/screenshots/bolt-connection-config.png | Bin 0 -> 46364 bytes
xdocs/images/screenshots/bolt-request.png | Bin 0 -> 139114 bytes
xdocs/usermanual/component_reference.xml | 49 ++++++
21 files changed, 747 insertions(+), 1 deletion(-)
diff --git a/bin/saveservice.properties b/bin/saveservice.properties
index e271e26..4f861db 100644
--- a/bin/saveservice.properties
+++ b/bin/saveservice.properties
@@ -247,6 +247,8 @@ MongoSourceElement=org.apache.jmeter.protocol.mongodb.config.MongoSourceElement
MonitorHealthVisualizer=org.apache.jmeter.visualizers.MonitorHealthVisualizer
NamePanel=org.apache.jmeter.gui.NamePanel
+BoltSampler=org.apache.jmeter.protocol.bolt.sampler.BoltSampler
+BoltConnectionElement=org.apache.jmeter.protocol.bolt.config.BoltConnectionElement
ObsoleteGui=org.apache.jmeter.config.gui.ObsoleteGui
OnceOnlyController=org.apache.jmeter.control.OnceOnlyController
OnceOnlyControllerGui=org.apache.jmeter.control.gui.OnceOnlyControllerGui
diff --git a/checksum.xml b/checksum.xml
index 53d9eae..65ca9a2 100644
--- a/checksum.xml
+++ b/checksum.xml
@@ -119,6 +119,7 @@
<trusted-key id='85911f425ec61b51' group='org.junit.vintage' />
<trusted-key id='82216a03caa86c78' group='org.mongodb' />
<trusted-key id='3f36885c24df4b75' group='org.mozilla' />
+ <trusted-key id='7ad289796be2ffe2' group='org.neo4j.driver' />
<trusted-key id='7c7d8456294423ba' group='org.objenesis' />
<trusted-key id='85911f425ec61b51' group='org.opentest4j' />
<trusted-key id='5f69ad087600b22c' group='org.ow2.asm' />
diff --git a/gradle.properties b/gradle.properties
index a94e5f3..2107116 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -85,6 +85,7 @@ log4j.version=2.12.1
mail.version=1.5.0-b01
mina-core.version=2.0.19
mongo-java-driver.version=2.11.3
+neo4j-java-driver.version=1.7.5
objenesis.version=2.6
oro.version=2.0.8
ph-commons.version=9.3.7
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 2c78a23..9b65857 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -32,6 +32,7 @@ include(
"src:generator",
"src:jorphan",
"src:licenses",
+ "src:protocol:bolt",
"src:protocol:ftp",
"src:protocol:http",
"src:protocol:java",
diff --git a/src/bom/build.gradle.kts b/src/bom/build.gradle.kts
index 4a47ae6..cdcec48 100644
--- a/src/bom/build.gradle.kts
+++ b/src/bom/build.gradle.kts
@@ -136,6 +136,7 @@ dependencies {
apiv("org.jsoup:jsoup")
apiv("org.mongodb:mongo-java-driver")
apiv("org.mozilla:rhino")
+ apiv("org.neo4j.driver:neo4j-java-driver")
apiv("org.objenesis:objenesis")
apiv("org.slf4j:jcl-over-slf4j", "slf4j")
apiv("org.slf4j:slf4j-api", "slf4j")
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 efd3e99..25d6354 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 = "1912808b50358c3afce8c54280f173b8fa8ba229"; // Expected value $NON-NLS-1$
+ static final String FILEVERSION = "6fd03656cf4997fe6b0af17fa8dc8469e563c93a"; // Expected value $NON-NLS-1$
private static String fileEncoding = ""; // read from properties file// $NON-NLS-1$
diff --git a/src/dist/build.gradle.kts b/src/dist/build.gradle.kts
index 597bded..cb1e6a3 100644
--- a/src/dist/build.gradle.kts
+++ b/src/dist/build.gradle.kts
@@ -37,6 +37,7 @@ var jars = arrayOf(
// ":src:examples",
":src:functions",
":src:jorphan",
+ ":src:protocol:bolt",
":src:protocol:ftp",
":src:protocol:http",
":src:protocol:java",
diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElement.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElement.java
new file mode 100644
index 0000000..a92a787
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElement.java
@@ -0,0 +1,121 @@
+/*
+ * 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.bolt.config;
+
+import org.apache.jmeter.config.ConfigElement;
+import org.apache.jmeter.testbeans.TestBean;
+import org.apache.jmeter.testbeans.TestBeanHelper;
+import org.apache.jmeter.testelement.AbstractTestElement;
+import org.apache.jmeter.testelement.TestStateListener;
+import org.apache.jmeter.threads.JMeterContextService;
+import org.apache.jmeter.threads.JMeterVariables;
+import org.neo4j.driver.v1.AuthTokens;
+import org.neo4j.driver.v1.Driver;
+import org.neo4j.driver.v1.GraphDatabase;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BoltConnectionElement extends AbstractTestElement
+ implements ConfigElement, TestStateListener, TestBean {
+
+ private static final Logger log = LoggerFactory.getLogger(BoltConnectionElement.class);
+ private String boltUri;
+ private String username;
+ private String password;
+ private Driver driver;
+
+ public static final String BOLT_CONNECTION = "boltConnection";
+
+ public BoltConnectionElement() {
+ }
+
+ @Override
+ public void addConfigElement(ConfigElement config) {
+
+ }
+
+ @Override
+ public boolean expectsModification() {
+ return false;
+ }
+
+ @Override
+ public void testStarted() {
+ this.setRunningVersion(true);
+ TestBeanHelper.prepare(this);
+ JMeterVariables variables = getThreadContext().getVariables();
+ if (variables.getObject(BOLT_CONNECTION) != null) {
+ log.error("Bolt connection already exists");
+ } else {
+ synchronized (this) {
+ driver = GraphDatabase.driver(getBoltUri(), AuthTokens.basic(getUsername(), getPassword()));
+ variables.putObject(BOLT_CONNECTION, driver);
+ }
+ }
+ }
+
+ @Override
+ public void testStarted(String host) {
+ testStarted();
+ }
+
+ @Override
+ public void testEnded() {
+ synchronized (this) {
+ if (driver != null) {
+ driver.close();
+ driver = null;
+ }
+ }
+
+ }
+
+ @Override
+ public void testEnded(String host) {
+ testEnded();
+ }
+
+ public String getBoltUri() {
+ return boltUri;
+ }
+
+ public void setBoltUri(String boltUri) {
+ this.boltUri = boltUri;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public static Driver getDriver() {
+ return (Driver) JMeterContextService.getContext().getVariables().getObject(BOLT_CONNECTION);
+ }
+}
diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementBeanInfo.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementBeanInfo.java
new file mode 100644
index 0000000..40b1543
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementBeanInfo.java
@@ -0,0 +1,57 @@
+/*
+ * 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.bolt.config;
+
+import java.beans.PropertyDescriptor;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+import org.apache.jmeter.testbeans.BeanInfoSupport;
+import org.apache.jmeter.testbeans.gui.TypeEditor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BoltConnectionElementBeanInfo extends BeanInfoSupport {
+
+ private static final Logger log = LoggerFactory.getLogger(BoltConnectionElementBeanInfo.class);
+
+ public BoltConnectionElementBeanInfo() {
+ super(BoltConnectionElement.class);
+
+ createPropertyGroup("connection", new String[] { "boltUri", "username", "password" });
+
+ PropertyDescriptor propertyDescriptor = property("boltUri");
+ propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+ propertyDescriptor.setValue(DEFAULT, "bolt://localhost:7687");
+ propertyDescriptor = property("username");
+ propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+ propertyDescriptor.setValue(DEFAULT, "neo4j");
+ propertyDescriptor = property("password", TypeEditor.PasswordEditor);
+ propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+ propertyDescriptor.setValue(DEFAULT, "");
+
+ if(log.isDebugEnabled()) {
+ String descriptorsAsString = Arrays.stream(getPropertyDescriptors())
+ .map(pd -> pd.getName() + "=" + pd.getDisplayName())
+ .collect(Collectors.joining(" ,"));
+ log.debug(descriptorsAsString);
+ }
+
+ }
+}
diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/AbstractBoltTestElement.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/AbstractBoltTestElement.java
new file mode 100644
index 0000000..89594aa
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/AbstractBoltTestElement.java
@@ -0,0 +1,52 @@
+/*
+ * 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.bolt.sampler;
+
+import org.apache.jmeter.testelement.AbstractTestElement;
+
+public abstract class AbstractBoltTestElement extends AbstractTestElement {
+
+ private String cypher;
+ private String params;
+ private boolean recordQueryResults;
+
+ public String getCypher() {
+ return cypher;
+ }
+
+ public void setCypher(String cypher) {
+ this.cypher = cypher;
+ }
+
+ public String getParams() {
+ return params;
+ }
+
+ public void setParams(String params) {
+ this.params = params;
+ }
+
+ public boolean isRecordQueryResults() {
+ return recordQueryResults;
+ }
+
+ public void setRecordQueryResults(boolean recordQueryResults) {
+ this.recordQueryResults = recordQueryResults;
+ }
+}
diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSampler.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSampler.java
new file mode 100644
index 0000000..dd24325
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSampler.java
@@ -0,0 +1,175 @@
+/*
+ * 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.bolt.sampler;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.jmeter.config.ConfigTestElement;
+import org.apache.jmeter.engine.util.ConfigMergabilityIndicator;
+import org.apache.jmeter.protocol.bolt.config.BoltConnectionElement;
+import org.apache.jmeter.samplers.Entry;
+import org.apache.jmeter.samplers.SampleResult;
+import org.apache.jmeter.samplers.Sampler;
+import org.apache.jmeter.testbeans.TestBean;
+import org.apache.jmeter.testelement.TestElement;
+import org.neo4j.driver.v1.Driver;
+import org.neo4j.driver.v1.Record;
+import org.neo4j.driver.v1.Session;
+import org.neo4j.driver.v1.StatementResult;
+import org.neo4j.driver.v1.exceptions.Neo4jException;
+import org.neo4j.driver.v1.summary.ResultSummary;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectReader;
+
+public class BoltSampler extends AbstractBoltTestElement implements Sampler, TestBean, ConfigMergabilityIndicator {
+
+ private static final Set<String> APPLICABLE_CONFIG_CLASSES = new HashSet<>(
+ Collections.singletonList("org.apache.jmeter.config.gui.SimpleConfigGui")); // $NON-NLS-1$
+
+ private static final ObjectReader objectMapper = new ObjectMapper().readerFor(new TypeReference<HashMap<String, Object>>() {});
+
+ @Override
+ public SampleResult sample(Entry e) {
+ SampleResult res = new SampleResult();
+ res.setSampleLabel(getName());
+ res.setSamplerData(request());
+ res.setDataType(SampleResult.TEXT);
+ res.setContentType("text/plain"); // $NON-NLS-1$
+ res.setDataEncoding(StandardCharsets.UTF_8.name());
+
+ Map<String, Object> params;
+ try {
+ params = getParamsAsMap();
+ } catch (IOException ex) {
+ return handleException(res, ex);
+ }
+
+ // Assume we will be successful
+ res.setSuccessful(true);
+ res.setResponseMessageOK();
+ res.setResponseCodeOK();
+
+ res.sampleStart();
+
+ try {
+ res.setResponseHeaders("Cypher request: " + getCypher());
+ res.setResponseData(execute(BoltConnectionElement.getDriver(), getCypher(), params), StandardCharsets.UTF_8.name());
+ } catch (Exception ex) {
+ res = handleException(res, ex);
+ } finally {
+ res.sampleEnd();
+ }
+ return res;
+ }
+
+ /**
+ * @see org.apache.jmeter.samplers.AbstractSampler#applies(org.apache.jmeter.config.ConfigTestElement)
+ */
+ @Override
+ public boolean applies(ConfigTestElement configElement) {
+ String guiClass = configElement.getProperty(TestElement.GUI_CLASS).getStringValue();
+ return APPLICABLE_CONFIG_CLASSES.contains(guiClass);
+ }
+
+ private String execute(Driver driver, String cypher, Map<String, Object> params) {
+ try (Session session = driver.session()) {
+ StatementResult statementResult = session.run(cypher, params);
+ return response(statementResult);
+ }
+ }
+
+ private SampleResult handleException(SampleResult res, Exception ex) {
+ res.setResponseMessage(ex.toString());
+ if (ex instanceof Neo4jException) {
+ res.setResponseCode(((Neo4jException)ex).code());
+ } else {
+ res.setResponseCode("500");
+ }
+ res.setResponseData(ObjectUtils.defaultIfNull(ex.getMessage(), "NO MESSAGE").getBytes());
+ res.setSuccessful(false);
+ return res;
+ }
+
+ private Map<String, Object> getParamsAsMap() throws IOException {
+ if (getParams() != null && getParams().length() > 0) {
+ return objectMapper.readValue(getParams());
+ } else {
+ return Collections.emptyMap();
+ }
+ }
+
+ private String request() {
+ StringBuilder request = new StringBuilder();
+ request.append("Query: \n")
+ .append(getCypher())
+ .append("\n")
+ .append("Parameters: \n")
+ .append(getParams());
+ return request.toString();
+ }
+
+ private String response(StatementResult result) {
+ StringBuilder response = new StringBuilder();
+ response.append("\nSummary:");
+ ResultSummary summary = result.summary();
+ response.append("\nConstraints Added: ")
+ .append(summary.counters().constraintsAdded())
+ .append("\nConstraints Removed: ")
+ .append(summary.counters().constraintsRemoved())
+ .append("\nContains Updates: ")
+ .append(summary.counters().containsUpdates())
+ .append("\nIndexes Added: ")
+ .append(summary.counters().indexesAdded())
+ .append("\nIndexes Removed: ")
+ .append(summary.counters().indexesRemoved())
+ .append("\nLabels Added: ")
+ .append(summary.counters().labelsAdded())
+ .append("\nLabels Removed: ")
+ .append(summary.counters().labelsRemoved())
+ .append("\nNodes Created: ")
+ .append(summary.counters().nodesCreated())
+ .append("\nNodes Deleted: ")
+ .append(summary.counters().nodesDeleted())
+ .append("\nRelationships Created: ")
+ .append(summary.counters().relationshipsCreated())
+ .append("\nRelationships Deleted: ")
+ .append(summary.counters().relationshipsDeleted());
+ response.append("\n\nRecords: ");
+ if (isRecordQueryResults()) {
+ for (Record record : result.list()) {
+ response.append("\n").append(record);
+ }
+ } else {
+ response.append("Skipped");
+ result.consume();
+ }
+
+
+ return response.toString();
+ }
+}
diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerBeanInfo.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerBeanInfo.java
new file mode 100644
index 0000000..5e6c114
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerBeanInfo.java
@@ -0,0 +1,26 @@
+/*
+ * 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.bolt.sampler;
+
+public class BoltSamplerBeanInfo extends BoltTestElementBeanInfoSupport {
+
+ public BoltSamplerBeanInfo() {
+ super(BoltSampler.class);
+ }
+}
diff --git a/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltTestElementBeanInfoSupport.java b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltTestElementBeanInfoSupport.java
new file mode 100644
index 0000000..5839b63
--- /dev/null
+++ b/src/protocol/bolt/src/main/java/org/apache/jmeter/protocol/bolt/sampler/BoltTestElementBeanInfoSupport.java
@@ -0,0 +1,50 @@
+/*
+ * 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.bolt.sampler;
+
+import java.beans.PropertyDescriptor;
+
+import org.apache.jmeter.testbeans.BeanInfoSupport;
+import org.apache.jmeter.testbeans.TestBean;
+import org.apache.jmeter.testbeans.gui.TypeEditor;
+
+public abstract class BoltTestElementBeanInfoSupport extends BeanInfoSupport {
+ /**
+ * Construct a BeanInfo for the given class.
+ *
+ * @param beanClass class for which to construct a BeanInfo
+ */
+ protected BoltTestElementBeanInfoSupport(Class<? extends TestBean> beanClass) {
+ super(beanClass);
+
+ createPropertyGroup("query", new String[] { "cypher","params","recordQueryResults"});
+
+ PropertyDescriptor propertyDescriptor = property("cypher", TypeEditor.TextAreaEditor);
+ propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+ propertyDescriptor.setValue(DEFAULT, "");
+
+ propertyDescriptor = property("params", TypeEditor.TextAreaEditor);
+ propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+ propertyDescriptor.setValue(DEFAULT, "{\"paramName\":\"paramValue\"}");
+
+ propertyDescriptor = property("recordQueryResults");
+ propertyDescriptor.setValue(NOT_UNDEFINED, Boolean.TRUE);
+ propertyDescriptor.setValue(DEFAULT, Boolean.FALSE);
+ }
+}
diff --git a/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementResources.properties b/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementResources.properties
new file mode 100644
index 0000000..b3050ef
--- /dev/null
+++ b/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/config/BoltConnectionElementResources.properties
@@ -0,0 +1,26 @@
+#
+# 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=Bolt Connection Configuration
+connection.displayName=Bolt Configuration
+boltUri.displayName=Bolt URI
+boltUri.shortDescription=Bolt URI
+username.displayName=Username
+username.shortDescription=Username
+password.displayName=Password
+password.shortDescription=Password
diff --git a/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerResources.properties b/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerResources.properties
new file mode 100644
index 0000000..c14ff03
--- /dev/null
+++ b/src/protocol/bolt/src/main/resources/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerResources.properties
@@ -0,0 +1,28 @@
+#
+# 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=Bolt Request
+query.displayName=Query
+cypher.displayName=Cypher Statement
+cypher.shortDescription=Cypher Statement
+params.displayName=Params
+params.shortDescription=Params
+recordQueryResults.displayName=Record Query Results
+recordQueryResults.shortDescription=Records the results of queries and displays in listeners such as View Results Tree, this iterates through the entire resultset. Use to debug only.
+
+
diff --git a/src/protocol/bolt/src/test/groovy/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerSpec.groovy b/src/protocol/bolt/src/test/groovy/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerSpec.groovy
new file mode 100644
index 0000000..cd43bc6
--- /dev/null
+++ b/src/protocol/bolt/src/test/groovy/org/apache/jmeter/protocol/bolt/sampler/BoltSamplerSpec.groovy
@@ -0,0 +1,123 @@
+/*
+ * 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.bolt.sampler
+
+import org.apache.jmeter.protocol.bolt.config.BoltConnectionElement
+import org.apache.jmeter.samplers.Entry
+import org.apache.jmeter.threads.JMeterContextService
+import org.apache.jmeter.threads.JMeterVariables
+import org.neo4j.driver.v1.Driver
+import org.neo4j.driver.v1.Session
+import org.neo4j.driver.v1.StatementResult
+import org.neo4j.driver.v1.exceptions.ClientException
+import org.neo4j.driver.v1.summary.ResultSummary
+import org.neo4j.driver.v1.summary.SummaryCounters
+
+import spock.lang.Specification
+
+class BoltSamplerSpec extends Specification {
+
+ BoltSampler sampler
+ Entry entry
+ Session session
+
+ def setup() {
+ sampler = new BoltSampler()
+ entry = new Entry()
+ def driver = Mock(Driver)
+ def boltConfig = new BoltConnectionElement()
+ def variables = new JMeterVariables()
+ // ugly but could not find a better way to pass the driver to the sampler...
+ variables.putObject(BoltConnectionElement.BOLT_CONNECTION, driver)
+ JMeterContextService.getContext().setVariables(variables)
+ entry.addConfigElement(boltConfig)
+ session = Mock(Session)
+ driver.session() >> session
+ }
+
+ def "should execute return success on successful query"() {
+ given:
+ sampler.setCypher("MATCH x")
+ session.run("MATCH x", [:]) >> getEmptyQueryResult()
+ when:
+ def response = sampler.sample(entry)
+ then:
+ response.isSuccessful()
+ response.isResponseCodeOK()
+ def str = response.getResponseDataAsString()
+ str.contains("Summary:")
+ str.endsWith("Records: Skipped")
+ response.getSampleCount() == 1
+ response.getErrorCount() == 0
+ response.getTime() > 0
+ }
+
+ def "should return error on failed query"() {
+ given:
+ sampler.setCypher("MATCH x")
+ session.run("MATCH x", [:]) >> { throw new RuntimeException("a message") }
+ when:
+ def response = sampler.sample(entry)
+ then:
+ !response.isSuccessful()
+ !response.isResponseCodeOK()
+ response.getResponseCode() == "500"
+ def str = response.getResponseDataAsString()
+ str.contains("a message")
+ response.getSampleCount() == 1
+ response.getErrorCount() == 1
+ response.getTime() > 0
+ }
+
+ def "should return error on invalid parameters"() {
+ given:
+ sampler.setCypher("MATCH x")
+ sampler.setParams("{invalid}")
+ when:
+ def response = sampler.sample(entry)
+ then:
+ !response.isSuccessful()
+ !response.isResponseCodeOK()
+ response.getResponseCode() == "500"
+ def str = response.getResponseDataAsString()
+ str.contains("Unexpected character")
+ response.getSampleCount() == 1
+ response.getErrorCount() == 1
+ response.getTime() == 0
+ }
+
+ def "should return db error code"() {
+ given:
+ sampler.setCypher("MATCH x")
+ session.run("MATCH x", [:]) >> { throw new ClientException("a code", "a message") }
+ when:
+ def response = sampler.sample(entry)
+ then:
+ response.getResponseCode() == "a code"
+ }
+
+ def getEmptyQueryResult() {
+ def queryResult = Mock(StatementResult)
+ def summary = Mock(ResultSummary)
+ queryResult.summary() >> summary
+ SummaryCounters counters = Mock(SummaryCounters)
+ summary.counters() >> counters
+ return queryResult
+ }
+}
diff --git a/src/protocol/bolt/src/test/java/org/apache/jmeter/resources/ResourceKeyUsageTestBolt.java b/src/protocol/bolt/src/test/java/org/apache/jmeter/resources/ResourceKeyUsageTestBolt.java
new file mode 100644
index 0000000..ee49316
--- /dev/null
+++ b/src/protocol/bolt/src/test/java/org/apache/jmeter/resources/ResourceKeyUsageTestBolt.java
@@ -0,0 +1,23 @@
+/*
+ * 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.resources;
+
+public class ResourceKeyUsageTestBolt extends ResourceKeyUsageTest {
+ // Test from the base class is used, so we just validate current module
+}
diff --git a/src/protocol/build.gradle.kts b/src/protocol/build.gradle.kts
index 88b8da0..959d26d 100644
--- a/src/protocol/build.gradle.kts
+++ b/src/protocol/build.gradle.kts
@@ -23,6 +23,15 @@ subprojects {
}
}
+project("bolt") {
+ dependencies {
+ implementation("org.neo4j.driver:neo4j-java-driver")
+ implementation("org.apache.commons:commons-lang3")
+ implementation("com.fasterxml.jackson.core:jackson-core")
+ implementation("com.fasterxml.jackson.core:jackson-databind")
+ }
+}
+
project("ftp") {
dependencies {
implementation("commons-net:commons-net:3.6")
diff --git a/xdocs/images/screenshots/bolt-connection-config.png b/xdocs/images/screenshots/bolt-connection-config.png
new file mode 100644
index 0000000..3afe34a
Binary files /dev/null and b/xdocs/images/screenshots/bolt-connection-config.png differ
diff --git a/xdocs/images/screenshots/bolt-request.png b/xdocs/images/screenshots/bolt-request.png
new file mode 100644
index 0000000..521772e
Binary files /dev/null and b/xdocs/images/screenshots/bolt-request.png differ
diff --git a/xdocs/usermanual/component_reference.xml b/xdocs/usermanual/component_reference.xml
index 819cb05..5fa5c32 100644
--- a/xdocs/usermanual/component_reference.xml
+++ b/xdocs/usermanual/component_reference.xml
@@ -1921,6 +1921,39 @@ MongoDB Script is more suitable for functional testing or test setup (setup/tear
<a href="#">^</a>
+
+<component name="Bolt Request" index="§-num;.1.22" width="711" height="488" screenshot="bolt-request.png">
+ <description>
+ <p>This sampler allows you to run Cypher queries through the Bolt protocol.</p>
+ <p>Before using this you need to set up a <complink name="Bolt Connection Configuration"/></p>
+ <p>Every request uses a connection acquired from the pool and returns it to the pool when the sampler completes.
+ The connection pool size use the driver defaults (~100) and is not configurable at the moment.</p>
+ <p>The measured response time corresponds to the "full" query execution, including both
+ the time to execute the cypher query AND the time to consume the results sent back by the database.</p>
+ </description>
+
+ <properties>
+ <property name="Name" required="No">Descriptive name for this sampler that is shown in the tree.</property>
+ <property name="Comments" required="No">Free text for additional details.</property>
+ <property name="Cypher statement" required="Yes">
+ The query to execute.
+ </property>
+ <property name="Params" required="No">The parameter values, JSON formatted.</property>
+ <property name="Record Query Results" required="No">
+ Whether to add or not query result data to the sampler response (default false).
+ Note that activating this has a memory overhead, use it wisely.
+ </property>
+ </properties>
+
+ <note>It is strongly advised to use query parameters, allowing the database to cache and reuse execution plans.</note>
+
+ <links>
+ <complink name="Bolt Connection Configuration"/>
+ </links>
+</component>
+
+<a href="#">^</a>
+
</section>
<section name="§-num;.2 Logic Controllers" anchor="logic_controllers">
@@ -4400,6 +4433,22 @@ DB db = MongoDBHolder.getDBFromSource("value of property MongoDB Source",
<a href="#">^</a>
+<component name="Bolt Connection Configuration" index="§-num;.4.21"
+ width="711" height="170" screenshot="bolt-connection-config.png">
+ <description>Creates a Bolt connection pool (used by <complink name="Bolt Request"/> Sampler)
+ from the supplied Connection settings.
+ </description>
+ <properties>
+ <property name="Name" required="No">Descriptive name for this sampler that is shown in the tree.</property>
+ <property name="Comments" required="No">Free text for additional details.</property>
+ <property name="Bolt URI" required="Yes">The database URI.</property>
+ <property name="Username" required="No">User account.</property>
+ <property name="Password" required="No">User credentials.</property>
+ </properties>
+</component>
+
+<a href="#">^</a>
+
</section>
<section name="§-num;.5 Assertions" anchor="assertions">