diff --git a/docs/src/main/asciidoc/_project-features-messaging.adoc b/docs/src/main/asciidoc/_project-features-messaging.adoc
index 29df58dcee..4c691a58ee 100644
--- a/docs/src/main/asciidoc/_project-features-messaging.adoc
+++ b/docs/src/main/asciidoc/_project-features-messaging.adoc
@@ -101,6 +101,7 @@ You can use one of the following four integration configurations:
* Spring Integration
* Spring Cloud Stream
* Spring AMQP
+* Spring JMS
Since we use Spring Boot, if you have added one of these libraries to the classpath, all
the messaging configuration is automatically set up.
@@ -1013,3 +1014,140 @@ stubrunner:
mockConnection: false
----
====
+
+[[features-messaging-stub-runner-jms]]
+=== Consumer Side Messaging With Spring JMS
+
+Spring Cloud Contract Stub Runner's messaging module provides an easy way to
+integrate with Spring JMS.
+
+The integration assumes that you have a running instance of a JMS broker (e.g. `activemq` embedded broker).
+
+[[features-messaging-stub-runner-jms-adding]]
+==== Adding the Runner to the Project
+
+You need to have both Spring JMS and Spring Cloud Contract Stub Runner on the classpath. Remember to annotate your test class
+with `@AutoConfigureStubRunner`.
+
+:input_name: input
+:output_name: output
+
+[[features-messaging-stub-runner-jms-example]]
+==== Examples
+
+Assume that the stub structure looks as follows:
+
+====
+[source,bash,indent=0]
+----
+├── stubs
+ ├── bookDeleted.groovy
+ ├── bookReturned1.groovy
+ └── bookReturned2.groovy
+
+----
+====
+
+Further assume the following test configuration:
+
+====
+[source,yml,indent=0]
+----
+stubrunner:
+ repository-root: stubs:classpath:/stubs/
+ ids: my:stubs
+ stubs-mode: remote
+spring:
+ activemq:
+ send-timeout: 1000
+ jms:
+ template:
+ receive-timeout: 1000
+----
+====
+
+Now consider the following contracts (we number them 1 and 2):
+
+====
+[source,groovy]
+----
+include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=sample_dsl,indent=0]
+----
+
+[source,groovy]
+----
+include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=sample_dsl_2,indent=0]
+----
+====
+
+[[features-messaging-stub-runner-jms-scenario1]]
+===== Scenario 1 (No Input Message)
+
+To trigger a message from the `return_book_1` label, we use the `StubTigger` interface, as follows:
+
+====
+[source,groovy]
+----
+include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_trigger,indent=0]
+----
+====
+
+Next, we want to listen to the output of the message sent to `{output_name}`:
+
+====
+[source,groovy]
+----
+include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_trigger_receive,indent=0]
+----
+====
+
+The received message would then pass the following assertions:
+
+====
+[source,groovy]
+----
+include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_trigger_message,indent=0]
+----
+====
+
+[[features-messaging-stub-runner-jms-scenario2]]
+===== Scenario 2 (Output Triggered by Input)
+
+Since the route is set for you, you can send a message to the `{output_name}` destination.
+
+====
+[source,groovy]
+----
+include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_send,indent=0]
+----
+====
+
+Next, we want to listen to the output of the message sent to `{output_name}`, as follows:
+
+====
+[source,groovy]
+----
+include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_receive,indent=0]
+----
+====
+
+The received message would pass the following assertions:
+
+====
+[source,groovy]
+----
+include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=client_receive_message,indent=0]
+----
+====
+
+[[features-messaging-stub-runner-jms-scenario3]]
+===== Scenario 3 (Input with No Output)
+
+Since the route is set for you, you can send a message to the `{output_name}` destination, as follows:
+
+====
+[source,groovy]
+----
+include::{tests_path}/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy[tags=trigger_no_output,indent=0]
+----
+====
\ No newline at end of file
diff --git a/spring-cloud-contract-stub-runner/pom.xml b/spring-cloud-contract-stub-runner/pom.xml
index 644290ee86..f4b26eaa55 100644
--- a/spring-cloud-contract-stub-runner/pom.xml
+++ b/spring-cloud-contract-stub-runner/pom.xml
@@ -65,6 +65,11 @@
jopt-simple
true
+
+ org.springframework.boot
+ spring-boot-starter-activemq
+ true
+
org.apache.camel
camel-spring-boot-starter
diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java
index a999b669ec..1976050127 100644
--- a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java
+++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/StubRunnerExecutor.java
@@ -255,6 +255,7 @@ class StubRunnerExecutor implements StubFinder {
OutputMessage outputMessage = groovyDsl.getOutputMessage();
DslProperty> body = outputMessage.getBody();
Headers headers = outputMessage.getHeaders();
+ // TODO: Json is harcoded here
this.contractVerifierMessaging.send(
JsonOutput.toJson(BodyExtractor.extractClientValueFromBody(
body == null ? null : body.getClientValue())),
diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsAccessor.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsAccessor.java
new file mode 100644
index 0000000000..265415bb55
--- /dev/null
+++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsAccessor.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.cloud.contract.stubrunner.messaging.jms;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jms.JMSException;
+import javax.jms.Message;
+import javax.jms.ObjectMessage;
+import javax.jms.StreamMessage;
+import javax.jms.TextMessage;
+
+final class StubRunnerJmsAccessor {
+
+ private StubRunnerJmsAccessor() {
+ throw new IllegalStateException("Can't instantiate an utility class");
+ }
+
+ static Object getBody(Message message) {
+ try {
+ return getPayload(message);
+ }
+ catch (JMSException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ static Map getHeaders(Message message) {
+ try {
+ return headers(message);
+ }
+ catch (JMSException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private static Map headers(Message message) throws JMSException {
+ Map headers = new HashMap<>();
+ if (message == null) {
+ return headers;
+ }
+ Enumeration enumeration = message.getPropertyNames();
+ while (enumeration.hasMoreElements()) {
+ Object element = enumeration.nextElement();
+ String asString = element.toString();
+ Object property = message.getObjectProperty(asString);
+ headers.put(asString, property);
+ }
+ return headers;
+ }
+
+ private static Object getPayload(Message message) throws JMSException {
+ if (message == null) {
+ return null;
+ }
+ else if (message instanceof TextMessage) {
+ return ((TextMessage) message).getText();
+ }
+ else if (message instanceof StreamMessage) {
+ return ((StreamMessage) message).readObject();
+ }
+ else if (message instanceof ObjectMessage) {
+ return ((ObjectMessage) message).getObject();
+ }
+ return message.getBody(Object.class);
+ }
+
+}
diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsConfiguration.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsConfiguration.java
new file mode 100644
index 0000000000..7a6b8aae62
--- /dev/null
+++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsConfiguration.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.cloud.contract.stubrunner.messaging.jms;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.jms.ConnectionFactory;
+import javax.jms.MessageListener;
+
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cloud.contract.spec.Contract;
+import org.springframework.cloud.contract.stubrunner.BatchStubRunner;
+import org.springframework.cloud.contract.stubrunner.StubConfiguration;
+import org.springframework.cloud.contract.verifier.util.MapConverter;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.jms.listener.DefaultMessageListenerContainer;
+import org.springframework.jms.listener.MessageListenerContainer;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.util.StringUtils;
+
+/**
+ * Spring Integration configuration that iterates over the downloaded Groovy DSLs and
+ * registers a flow for each DSL.
+ *
+ * @author Marcin Grzejszczak
+ */
+@Configuration
+@ConditionalOnClass(JmsTemplate.class)
+@ConditionalOnProperty(name = "stubrunner.jms.enabled", havingValue = "true",
+ matchIfMissing = true)
+public class StubRunnerJmsConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean(name = "stubFlowRegistrar")
+ public FlowRegistrar stubFlowRegistrar(ConfigurableListableBeanFactory beanFactory,
+ BatchStubRunner batchStubRunner) {
+ Map> contracts = batchStubRunner
+ .getContracts();
+ for (Entry> entry : contracts
+ .entrySet()) {
+ StubConfiguration key = entry.getKey();
+ Collection value = entry.getValue();
+ String name = key.getGroupId() + "_" + key.getArtifactId();
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ for (Contract dsl : value) {
+ if (dsl == null) {
+ continue;
+ }
+ if (dsl.getInput() != null && dsl.getInput().getMessageFrom() != null
+ && StringUtils.hasText(
+ dsl.getInput().getMessageFrom().getClientValue())) {
+ String from = dsl.getInput().getMessageFrom().getClientValue();
+ map.add(from, dsl);
+ }
+ }
+ for (Entry> entries : map.entrySet()) {
+ List matchingContracts = entries.getValue();
+ final String flowName = name + "_" + entries.getKey() + "_"
+ + Math.abs(matchingContracts.hashCode());
+ // listener
+ StubRunnerJmsRouter router = new StubRunnerJmsRouter(matchingContracts,
+ beanFactory);
+ StubRunnerJmsRouter listener = (StubRunnerJmsRouter) beanFactory
+ .initializeBean(router, flowName);
+ beanFactory.registerSingleton(flowName, listener);
+ registerContainers(beanFactory, matchingContracts, flowName, listener);
+ }
+
+ }
+ return new FlowRegistrar();
+ }
+
+ private void registerContainers(ConfigurableListableBeanFactory beanFactory,
+ List matchingContracts, String flowName,
+ StubRunnerJmsRouter listener) {
+ // listener's container
+ ConnectionFactory connectionFactory = beanFactory
+ .getBean(ConnectionFactory.class);
+ for (Contract matchingContract : matchingContracts) {
+ if (matchingContract.getInput() == null) {
+ continue;
+ }
+ String destination = MapConverter.getStubSideValuesForNonBody(
+ matchingContract.getInput().getMessageFrom()).toString();
+ MessageListenerContainer container = listenerContainer(destination,
+ connectionFactory, listener);
+ String containerName = flowName + ".container";
+ Object initializedContainer = beanFactory.initializeBean(container,
+ containerName);
+ beanFactory.registerSingleton(containerName, initializedContainer);
+ }
+ }
+
+ private MessageListenerContainer listenerContainer(String queueName,
+ ConnectionFactory connectionFactory, MessageListener listener) {
+ DefaultMessageListenerContainer container = new DefaultMessageListenerContainer();
+ container.setConnectionFactory(connectionFactory);
+ container.setDestinationName(queueName);
+ container.setMessageListener(listener);
+ return container;
+ }
+
+ static class FlowRegistrar {
+
+ }
+
+}
diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsMessageSelector.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsMessageSelector.java
new file mode 100644
index 0000000000..90b7eb38a2
--- /dev/null
+++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsMessageSelector.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.cloud.contract.stubrunner.messaging.jms;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.regex.Pattern;
+
+import javax.jms.Message;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.jayway.jsonpath.DocumentContext;
+import com.jayway.jsonpath.JsonPath;
+import com.toomuchcoding.jsonassert.JsonAssertion;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.cloud.contract.spec.Contract;
+import org.springframework.cloud.contract.spec.internal.BodyMatcher;
+import org.springframework.cloud.contract.spec.internal.BodyMatchers;
+import org.springframework.cloud.contract.spec.internal.FromFileProperty;
+import org.springframework.cloud.contract.spec.internal.Header;
+import org.springframework.cloud.contract.spec.internal.RegexProperty;
+import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper;
+import org.springframework.cloud.contract.verifier.util.ContentType;
+import org.springframework.cloud.contract.verifier.util.ContentUtils;
+import org.springframework.cloud.contract.verifier.util.JsonPaths;
+import org.springframework.cloud.contract.verifier.util.JsonToJsonPathsConverter;
+import org.springframework.cloud.contract.verifier.util.MapConverter;
+import org.springframework.cloud.contract.verifier.util.MethodBufferingJsonVerifiable;
+
+/**
+ * Passes through a message that matches the one defined in the DSL.
+ *
+ * @author Marcin Grzejszczak
+ * @author Tim Ysewyn
+ */
+class StubRunnerJmsMessageSelector {
+
+ private static final Map CACHE = Collections
+ .synchronizedMap(new WeakHashMap<>());
+
+ private static final Log log = LogFactory.getLog(StubRunnerJmsMessageSelector.class);
+
+ private final List groovyDsls;
+
+ private final ContractVerifierObjectMapper objectMapper = new ContractVerifierObjectMapper();
+
+ StubRunnerJmsMessageSelector(List groovyDsls) {
+ this.groovyDsls = groovyDsls;
+ }
+
+ Contract matchingContract(Message message) {
+ if (CACHE.containsKey(message)) {
+ return CACHE.get(message);
+ }
+ Contract contract = getContract(message);
+ if (contract != null) {
+ CACHE.put(message, contract);
+ }
+ return contract;
+ }
+
+ void updateCache(Message message, Contract contract) {
+ CACHE.put(message, contract);
+ }
+
+ private Contract getContract(Message message) {
+ for (Contract groovyDsl : this.groovyDsls) {
+ Contract contract = matchContract(message, groovyDsl);
+ if (contract != null) {
+ return contract;
+ }
+ }
+ return null;
+ }
+
+ private Contract matchContract(Message message, Contract groovyDsl) {
+ List unmatchedHeaders = headersMatch(message, groovyDsl);
+ if (!unmatchedHeaders.isEmpty()) {
+ if (log.isDebugEnabled()) {
+ log.debug("Contract [" + groovyDsl
+ + "] hasn't matched the following headers " + unmatchedHeaders);
+ }
+ return null;
+ }
+ Object inputMessage = StubRunnerJmsAccessor.getBody(message);
+ Object dslBody = MapConverter
+ .getStubSideValues(groovyDsl.getInput().getMessageBody());
+ if (dslBody instanceof FromFileProperty) {
+ if (log.isDebugEnabled()) {
+ log.debug("Will compare file content");
+ }
+ FromFileProperty property = (FromFileProperty) dslBody;
+ if (property.isString()) {
+ // continue processing as if body was pure string
+ dslBody = property.asString();
+ }
+ else if (!(inputMessage instanceof byte[])) {
+ if (log.isDebugEnabled()) {
+ log.debug(
+ "Contract provided byte comparison, but the input message is of type ["
+ + inputMessage.getClass()
+ + "]. Can't compare the two.");
+ }
+ return null;
+ }
+ else {
+ boolean matches = Arrays.equals(property.asBytes(),
+ (byte[]) inputMessage);
+ if (log.isDebugEnabled() && !matches) {
+ log.debug(
+ "Contract provided byte comparison, but the byte arrays don't match");
+ }
+ return matches ? groovyDsl : null;
+ }
+ }
+ if (matchViaContent(groovyDsl, inputMessage, dslBody)) {
+ return groovyDsl;
+ }
+ return null;
+ }
+
+ private boolean matchViaContent(Contract groovyDsl, Object inputMessage,
+ Object dslBody) {
+ boolean matches;
+ ContentType type = ContentUtils.getClientContentType(inputMessage,
+ groovyDsl.getInput().getMessageHeaders());
+ if (type == ContentType.JSON) {
+ BodyMatchers matchers = groovyDsl.getInput().getBodyMatchers();
+ matches = matchesForJsonPayload(groovyDsl, inputMessage, matchers, dslBody);
+ }
+ else if (dslBody instanceof RegexProperty && inputMessage instanceof String) {
+ Pattern pattern = ((RegexProperty) dslBody).getPattern();
+ matches = pattern.matcher((String) inputMessage).matches();
+ bodyUnmatchedLog(dslBody, matches, pattern);
+ }
+ else {
+ matches = dslBody.equals(inputMessage);
+ bodyUnmatchedLog(dslBody, matches, inputMessage);
+ }
+ return matches;
+ }
+
+ private void bodyUnmatchedLog(Object dslBody, boolean matches, Object pattern) {
+ if (log.isDebugEnabled() && !matches) {
+ log.debug("Body was supposed to " + unmatchedText(pattern)
+ + " but the value is [" + dslBody.toString() + "]");
+ }
+ }
+
+ private boolean matchesForJsonPayload(Contract groovyDsl, Object inputMessage,
+ BodyMatchers matchers, Object dslBody) {
+ Object matchingInputMessage = JsonToJsonPathsConverter
+ .removeMatchingJsonPaths(dslBody, matchers);
+ JsonPaths jsonPaths = JsonToJsonPathsConverter
+ .transformToJsonPathWithStubsSideValuesAndNoArraySizeCheck(
+ matchingInputMessage);
+ DocumentContext parsedJson;
+ try {
+ parsedJson = JsonPath
+ .parse(this.objectMapper.writeValueAsString(inputMessage));
+ }
+ catch (JsonProcessingException e) {
+ throw new IllegalStateException("Cannot serialize to JSON", e);
+ }
+ List unmatchedJsonPath = new ArrayList<>();
+ boolean matches = true;
+ for (MethodBufferingJsonVerifiable path : jsonPaths) {
+ matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, path.jsonPath());
+ }
+ if (matchers != null && matchers.hasMatchers()) {
+ for (BodyMatcher matcher : matchers.matchers()) {
+ String jsonPath = JsonToJsonPathsConverter
+ .convertJsonPathAndRegexToAJsonPath(matcher, dslBody);
+ matches &= matchesJsonPath(unmatchedJsonPath, parsedJson, jsonPath);
+ }
+ }
+ if (!unmatchedJsonPath.isEmpty()) {
+ if (log.isDebugEnabled()) {
+ log.debug("Contract [" + groovyDsl + "] didn't match the body due to "
+ + unmatchedJsonPath);
+ }
+ }
+ return matches;
+ }
+
+ private boolean matchesJsonPath(List unmatchedJsonPath,
+ DocumentContext parsedJson, String jsonPath) {
+ try {
+ JsonAssertion.assertThat(parsedJson).matchesJsonPath(jsonPath);
+ return true;
+ }
+ catch (Exception e) {
+ unmatchedJsonPath.add(e.getLocalizedMessage());
+ return false;
+ }
+ }
+
+ private List headersMatch(Message message, Contract groovyDsl) {
+ List unmatchedHeaders = new ArrayList<>();
+ Map headers = StubRunnerJmsAccessor.getHeaders(message);
+ for (Header it : groovyDsl.getInput().getMessageHeaders().getEntries()) {
+ String name = it.getName();
+ Object value = it.getClientValue();
+ Object valueInHeader = headers.get(name);
+ boolean matches;
+ if (value instanceof RegexProperty) {
+ Pattern pattern = ((RegexProperty) value).getPattern();
+ matches = pattern.matcher(valueInHeader.toString()).matches();
+ }
+ else {
+ matches = valueInHeader != null
+ && valueInHeader.toString().equals(value.toString());
+ }
+ if (!matches) {
+ unmatchedHeaders.add("Header with name [" + name + "] was supposed to "
+ + unmatchedText(value) + " but the value is ["
+ + (valueInHeader != null ? valueInHeader.toString() : "null")
+ + "]");
+ }
+ }
+ return unmatchedHeaders;
+ }
+
+ private String unmatchedText(Object expectedValue) {
+ return expectedValue instanceof RegexProperty
+ ? "match pattern [" + ((RegexProperty) expectedValue).pattern() + "]"
+ : "be equal to [" + expectedValue + "]";
+ }
+
+}
diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsRouter.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsRouter.java
new file mode 100644
index 0000000000..62839a3b7e
--- /dev/null
+++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsRouter.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.cloud.contract.stubrunner.messaging.jms;
+
+import java.util.List;
+
+import javax.jms.JMSException;
+import javax.jms.Message;
+import javax.jms.MessageListener;
+
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.cloud.contract.spec.Contract;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.jms.core.MessagePostProcessor;
+
+/**
+ * @author Marcin Grzejszczak
+ */
+class StubRunnerJmsRouter implements MessageListener {
+
+ private final StubRunnerJmsMessageSelector selector;
+
+ private final BeanFactory beanFactory;
+
+ private final List contracts;
+
+ private JmsTemplate jmsTemplate;
+
+ StubRunnerJmsRouter(List groovyDsls, BeanFactory beanFactory) {
+ this.selector = new StubRunnerJmsMessageSelector(groovyDsls);
+ this.beanFactory = beanFactory;
+ this.contracts = groovyDsls;
+ }
+
+ @Override
+ public void onMessage(javax.jms.Message message) {
+ Contract dsl = this.selector.matchingContract(message);
+ if (dsl != null && dsl.getOutputMessage() != null
+ && dsl.getOutputMessage().getSentTo() != null) {
+ String destination = dsl.getOutputMessage().getSentTo().getClientValue();
+ jmsTemplate().send(destination,
+ session -> new StubRunnerJmsTransformer(this.contracts)
+ .transform(session, dsl));
+ }
+ }
+
+ private JmsTemplate jmsTemplate() {
+ if (this.jmsTemplate == null) {
+ this.jmsTemplate = this.beanFactory.getBean(JmsTemplate.class);
+ }
+ return this.jmsTemplate;
+ }
+
+}
+
+class ReplyToProcessor implements MessagePostProcessor {
+
+ @Override
+ public javax.jms.Message postProcessMessage(Message message) throws JMSException {
+ message.setStringProperty("requiresReply", "no");
+ return message;
+ }
+
+}
diff --git a/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsTransformer.java b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsTransformer.java
new file mode 100644
index 0000000000..0b5d63d143
--- /dev/null
+++ b/spring-cloud-contract-stub-runner/src/main/java/org/springframework/cloud/contract/stubrunner/messaging/jms/StubRunnerJmsTransformer.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.cloud.contract.stubrunner.messaging.jms;
+
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+import javax.jms.BytesMessage;
+import javax.jms.JMSException;
+import javax.jms.Message;
+import javax.jms.Session;
+
+import org.springframework.cloud.contract.spec.Contract;
+import org.springframework.cloud.contract.spec.internal.FromFileProperty;
+import org.springframework.cloud.contract.verifier.util.BodyExtractor;
+
+/**
+ * Sends forward a message defined in the DSL.
+ *
+ * @author Marcin Grzejszczak
+ */
+class StubRunnerJmsTransformer {
+
+ private final StubRunnerJmsMessageSelector selector;
+
+ StubRunnerJmsTransformer(List groovyDsls) {
+ this.selector = new StubRunnerJmsMessageSelector(groovyDsls);
+ }
+
+ public Message transform(Session session, Contract groovyDsl) {
+ Object outputBody = outputBody(groovyDsl);
+ Map headers = groovyDsl.getOutputMessage().getHeaders()
+ .asStubSideMap();
+ Message newMessage = createMessage(session, outputBody);
+ setHeaders(newMessage, headers);
+ this.selector.updateCache(newMessage, groovyDsl);
+ return newMessage;
+ }
+
+ private Object outputBody(Contract groovyDsl) {
+ Object outputBody = BodyExtractor
+ .extractClientValueFromBody(groovyDsl.getOutputMessage().getBody());
+ if (outputBody instanceof FromFileProperty) {
+ FromFileProperty property = (FromFileProperty) outputBody;
+ return property.asBytes();
+ }
+ return BodyExtractor.extractStubValueFrom(outputBody);
+ }
+
+ Contract matchingContract(Message source) {
+ return this.selector.matchingContract(source);
+ }
+
+ private Message createMessage(Session session, Object payload) {
+ try {
+ if (payload instanceof String) {
+ return session.createTextMessage((String) payload);
+ }
+ else if (payload instanceof byte[]) {
+ BytesMessage bytesMessage = session.createBytesMessage();
+ bytesMessage.writeBytes((byte[]) payload);
+ return bytesMessage;
+ }
+ else if (payload instanceof Serializable) {
+ return session.createObjectMessage((Serializable) payload);
+ }
+ return session.createMessage();
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private void setHeaders(Message message, Map headers) {
+ for (Map.Entry entry : headers.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ try {
+ if (value instanceof String) {
+ message.setStringProperty(key, (String) value);
+ }
+ else if (value instanceof Boolean) {
+ message.setBooleanProperty(key, (Boolean) value);
+ }
+ else {
+ message.setObjectProperty(key, value);
+ }
+ }
+ catch (JMSException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+ }
+
+}
diff --git a/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories
index f0d56cf795..6b0934fcd7 100644
--- a/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-contract-stub-runner/src/main/resources/META-INF/spring.factories
@@ -4,6 +4,7 @@ org.springframework.cloud.contract.stubrunner.spring.StubRunnerConfiguration,\
org.springframework.cloud.contract.stubrunner.spring.cloud.StubRunnerSpringCloudAutoConfiguration,\
org.springframework.cloud.contract.stubrunner.spring.cloud.ribbon.StubRunnerRibbonAutoConfiguration,\
org.springframework.cloud.contract.stubrunner.messaging.integration.StubRunnerIntegrationConfiguration,\
+org.springframework.cloud.contract.stubrunner.messaging.jms.StubRunnerJmsConfiguration,\
org.springframework.cloud.contract.stubrunner.messaging.stream.StubRunnerStreamConfiguration,\
org.springframework.cloud.contract.stubrunner.spring.cloud.zookeeper.StubRunnerSpringCloudZookeeperAutoConfiguration,\
org.springframework.cloud.contract.stubrunner.spring.cloud.eureka.StubRunnerSpringCloudEurekaAutoConfiguration,\
diff --git a/spring-cloud-contract-verifier/pom.xml b/spring-cloud-contract-verifier/pom.xml
index db66f6afca..1ca7d39637 100644
--- a/spring-cloud-contract-verifier/pom.xml
+++ b/spring-cloud-contract-verifier/pom.xml
@@ -31,6 +31,16 @@
spring-messaging
true
+
+ org.springframework
+ spring-jms
+ true
+
+
+ jakarta.jms
+ jakarta.jms-api
+ true
+
org.apache.camel
camel-spring-boot-starter
diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java
index 77346e64aa..ac5dbe58c0 100644
--- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java
+++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/CamelStubMessages.java
@@ -30,12 +30,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
-import org.springframework.stereotype.Component;
/**
* @author Marcin Grzejszczak
*/
-@Component
public class CamelStubMessages implements MessageVerifier {
private static final Logger log = LoggerFactory.getLogger(CamelStubMessages.class);
diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java
index 3563ca2dda..670db3ea6f 100644
--- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java
+++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/camel/ContractVerifierCamelConfiguration.java
@@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
+import org.springframework.cloud.contract.verifier.messaging.jms.ContractVerifierJmsConfiguration;
import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -38,9 +39,9 @@ import org.springframework.context.annotation.Import;
@Configuration
@ConditionalOnClass(Message.class)
@Import(CamelAutoConfiguration.class)
-@ConditionalOnProperty(name = "stubrunner.camel.enabled", havingValue = "true",
- matchIfMissing = true)
-@AutoConfigureBefore(NoOpContractVerifierAutoConfiguration.class)
+@ConditionalOnProperty(name = "stubrunner.camel.enabled", havingValue = "true", matchIfMissing = true)
+@AutoConfigureBefore({ NoOpContractVerifierAutoConfiguration.class,
+ ContractVerifierJmsConfiguration.class })
public class ContractVerifierCamelConfiguration {
@Bean
diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/ContractVerifierJmsConfiguration.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/ContractVerifierJmsConfiguration.java
new file mode 100644
index 0000000000..d56ea8cf9d
--- /dev/null
+++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/ContractVerifierJmsConfiguration.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.cloud.contract.verifier.messaging.jms;
+
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jms.JMSException;
+import javax.jms.Message;
+import javax.jms.ObjectMessage;
+import javax.jms.StreamMessage;
+import javax.jms.TextMessage;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
+import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage;
+import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging;
+import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jms.core.JmsTemplate;
+
+/**
+ * @author Marcin Grzejszczak
+ */
+@Configuration
+@ConditionalOnClass(JmsTemplate.class)
+@ConditionalOnProperty(name = "stubrunner.jms.enabled", havingValue = "true",
+ matchIfMissing = true)
+@AutoConfigureBefore(NoOpContractVerifierAutoConfiguration.class)
+public class ContractVerifierJmsConfiguration {
+
+ @Bean
+ @ConditionalOnMissingBean
+ MessageVerifier contractVerifierJmsMessageExchange(
+ ObjectProvider jmsTemplateProvider) {
+ JmsTemplate jmsTemplate = jmsTemplateProvider.getIfAvailable(JmsTemplate::new);
+ return new JmsStubMessages(jmsTemplate);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean
+ public ContractVerifierMessaging contractVerifierJmsMessaging(
+ MessageVerifier exchange) {
+ return new ContractVerifierJmsHelper(exchange);
+ }
+
+}
+
+class ContractVerifierJmsHelper extends ContractVerifierMessaging {
+
+ private static final Log log = LogFactory.getLog(ContractVerifierJmsHelper.class);
+
+ ContractVerifierJmsHelper(MessageVerifier exchange) {
+ super(exchange);
+ }
+
+ @Override
+ protected ContractVerifierMessage convert(Message message) {
+ try {
+ Map headers = headers(message);
+ return new ContractVerifierMessage(getPayload(message), headers);
+ }
+ catch (JMSException ex) {
+ log.warn("An exception occurred while trying to convert the JMS message", ex);
+ throw new IllegalStateException(ex);
+ }
+ }
+
+ private Map headers(Message message) throws JMSException {
+ Map headers = new HashMap<>();
+ if (message == null) {
+ return headers;
+ }
+ Enumeration enumeration = message.getPropertyNames();
+ while (enumeration.hasMoreElements()) {
+ Object element = enumeration.nextElement();
+ String asString = element.toString();
+ Object property = message.getObjectProperty(asString);
+ headers.put(asString, property);
+ }
+ return headers;
+ }
+
+ private Object getPayload(Message message) throws JMSException {
+ if (message == null) {
+ return null;
+ }
+ else if (message instanceof TextMessage) {
+ return ((TextMessage) message).getText();
+ }
+ else if (message instanceof StreamMessage) {
+ return ((StreamMessage) message).readObject();
+ }
+ else if (message instanceof ObjectMessage) {
+ return ((ObjectMessage) message).getObject();
+ }
+ return message.getBody(Object.class);
+ }
+
+}
diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/JmsStubMessages.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/JmsStubMessages.java
new file mode 100644
index 0000000000..6bfbcf7811
--- /dev/null
+++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/messaging/jms/JmsStubMessages.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.cloud.contract.verifier.messaging.jms;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.jms.BytesMessage;
+import javax.jms.JMSException;
+import javax.jms.Message;
+import javax.jms.Session;
+
+import org.springframework.cloud.contract.verifier.messaging.MessageVerifier;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.jms.core.MessagePostProcessor;
+
+class JmsStubMessages implements MessageVerifier {
+
+ private final JmsTemplate jmsTemplate;
+
+ JmsStubMessages(JmsTemplate jmsTemplate) {
+ this.jmsTemplate = jmsTemplate;
+ }
+
+ @Override
+ public void send(Message message, String destination) {
+ jmsTemplate.convertAndSend(destination, message, new ReplyToProcessor());
+ }
+
+ @Override
+ public Message receive(String destination, long timeout, TimeUnit timeUnit) {
+ jmsTemplate.setReceiveTimeout(timeUnit.toMillis(timeout));
+ return jmsTemplate.receive(destination);
+ }
+
+ @Override
+ public Message receive(String destination) {
+ return receive(destination, 5, TimeUnit.SECONDS);
+ }
+
+ @Override
+ public void send(Object payload, Map headers, String destination) {
+ jmsTemplate.send(destination, session -> {
+ Message message = createMessage(session, payload);
+ setHeaders(message, headers);
+ return message;
+ });
+ }
+
+ private Message createMessage(Session session, Object payload) throws JMSException {
+ if (payload instanceof String) {
+ return session.createTextMessage((String) payload);
+ }
+ else if (payload instanceof byte[]) {
+ BytesMessage bytesMessage = session.createBytesMessage();
+ bytesMessage.writeBytes((byte[]) payload);
+ return bytesMessage;
+ }
+ else if (payload instanceof Serializable) {
+ return session.createObjectMessage((Serializable) payload);
+ }
+ return session.createMessage();
+ }
+
+ private void setHeaders(Message message, Map headers) {
+ for (Map.Entry entry : headers.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ try {
+ if (value instanceof String) {
+ message.setStringProperty(key, (String) value);
+ }
+ else if (value instanceof Boolean) {
+ message.setBooleanProperty(key, (Boolean) value);
+ }
+ else {
+ message.setObjectProperty(key, value);
+ }
+ }
+ catch (JMSException ex) {
+ throw new IllegalStateException(ex);
+ }
+ }
+ }
+
+}
+
+class ReplyToProcessor implements MessagePostProcessor {
+
+ @Override
+ public Message postProcessMessage(Message message) throws JMSException {
+ message.setStringProperty("requiresReply", "no");
+ return message;
+ }
+
+}
diff --git a/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories b/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories
index 99c7544594..275d99c1ae 100644
--- a/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories
+++ b/spring-cloud-contract-verifier/src/main/resources/META-INF/spring.factories
@@ -5,4 +5,5 @@ org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifi
org.springframework.cloud.contract.verifier.messaging.amqp.ContractVerifierAmqpAutoConfiguration,\
org.springframework.cloud.contract.verifier.messaging.amqp.RabbitMockConnectionFactoryAutoConfiguration,\
org.springframework.cloud.contract.verifier.messaging.camel.ContractVerifierCamelConfiguration,\
+org.springframework.cloud.contract.verifier.messaging.jms.ContractVerifierJmsConfiguration,\
org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration
diff --git a/tests/pom.xml b/tests/pom.xml
index d703e19874..b4442fc187 100644
--- a/tests/pom.xml
+++ b/tests/pom.xml
@@ -49,6 +49,7 @@
samples-messaging-stream
samples-messaging-integration
samples-messaging-amqp
+ samples-messaging-jms
spring-cloud-contract-stub-runner-camel
spring-cloud-contract-stub-runner-boot-eureka
spring-cloud-contract-stub-runner-boot-zookeeper
@@ -56,6 +57,7 @@
spring-cloud-contract-stub-runner-integration
spring-cloud-contract-stub-runner-stream
spring-cloud-contract-stub-runner-amqp
+ spring-cloud-contract-stub-runner-jms
@@ -70,6 +72,7 @@
samples-messaging-stream
samples-messaging-integration
samples-messaging-amqp
+ samples-messaging-jms
spring-cloud-contract-stub-runner-camel
spring-cloud-contract-stub-runner-boot-eureka
spring-cloud-contract-stub-runner-boot-zookeeper
@@ -79,6 +82,7 @@
spring-cloud-contract-stub-runner-integration
spring-cloud-contract-stub-runner-stream
spring-cloud-contract-stub-runner-amqp
+ spring-cloud-contract-stub-runner-jms
diff --git a/tests/samples-messaging-jms/.springBeans b/tests/samples-messaging-jms/.springBeans
new file mode 100644
index 0000000000..b74cfc29b8
--- /dev/null
+++ b/tests/samples-messaging-jms/.springBeans
@@ -0,0 +1,16 @@
+
+
+ 1
+
+
+
+
+
+
+ java:com.example.fraud.CamelMessagingApplication
+
+
+
+
+
+
diff --git a/tests/samples-messaging-jms/pom.xml b/tests/samples-messaging-jms/pom.xml
new file mode 100644
index 0000000000..b4a1a8e5e3
--- /dev/null
+++ b/tests/samples-messaging-jms/pom.xml
@@ -0,0 +1,45 @@
+
+
+ 4.0.0
+
+ org.springframework.cloud
+ spring-cloud-contract-tests
+ 2.2.0.BUILD-SNAPSHOT
+ ..
+
+ spring-cloud-contract-sample-jms
+ jar
+ Spring Cloud Contract Sample Jms
+ Spring Cloud Contract Sample Jms
+
+
+ org.springframework.boot
+ spring-boot-starter-activemq
+
+
+ org.springframework.cloud
+ spring-cloud-contract-verifier
+ test
+
+
+ org.spockframework
+ spock-spring
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+ org.codehaus.gmavenplus
+ gmavenplus-plugin
+
+
+
+
diff --git a/tests/samples-messaging-jms/src/main/java/com/example/BookDeleted.java b/tests/samples-messaging-jms/src/main/java/com/example/BookDeleted.java
new file mode 100644
index 0000000000..118dd4fd39
--- /dev/null
+++ b/tests/samples-messaging-jms/src/main/java/com/example/BookDeleted.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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 com.example;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@SuppressWarnings("serial")
+public class BookDeleted implements Serializable {
+
+ public final String bookName;
+
+ @JsonCreator
+ public BookDeleted(@JsonProperty("bookName") String bookName) {
+ this.bookName = bookName;
+ }
+
+}
diff --git a/tests/samples-messaging-jms/src/main/java/com/example/BookDeleter.java b/tests/samples-messaging-jms/src/main/java/com/example/BookDeleter.java
new file mode 100644
index 0000000000..32b0877459
--- /dev/null
+++ b/tests/samples-messaging-jms/src/main/java/com/example/BookDeleter.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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 com.example;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.jms.JMSException;
+import javax.jms.Message;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.stereotype.Component;
+
+@Component
+public class BookDeleter {
+
+ private static final Logger log = LoggerFactory.getLogger(BookDeleter.class);
+
+ private AtomicBoolean bookSuccessfulyDeleted = new AtomicBoolean(false);
+
+ /**
+ * Scenario for "should generate tests triggered by a message": client side: if sends
+ * a message to input.messageFrom then message will be sent to output.messageFrom
+ * server side: will send a message to input, verify the message contents and await
+ * upon receiving message on the output messageFrom
+ */
+ @JmsListener(destination = "delete")
+ public void bookDeleted(Message message) throws JMSException {
+ log.info("Deleting book " + message);
+ this.bookSuccessfulyDeleted.set(true);
+ log.info("Book successfuly deleted [" + this.bookSuccessfulyDeleted + "]");
+ }
+
+}
diff --git a/tests/samples-messaging-jms/src/main/java/com/example/BookReturned.java b/tests/samples-messaging-jms/src/main/java/com/example/BookReturned.java
new file mode 100644
index 0000000000..81fc0a993e
--- /dev/null
+++ b/tests/samples-messaging-jms/src/main/java/com/example/BookReturned.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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 com.example;
+
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@SuppressWarnings("serial")
+public class BookReturned implements Serializable {
+
+ public final String bookName;
+
+ @JsonCreator
+ BookReturned(@JsonProperty("bookName") String bookName) {
+ this.bookName = bookName;
+ }
+
+}
diff --git a/tests/samples-messaging-jms/src/main/java/com/example/BookService.java b/tests/samples-messaging-jms/src/main/java/com/example/BookService.java
new file mode 100644
index 0000000000..154cf489b7
--- /dev/null
+++ b/tests/samples-messaging-jms/src/main/java/com/example/BookService.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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 com.example;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.springframework.jms.annotation.JmsListener;
+import org.springframework.jms.core.JmsTemplate;
+import org.springframework.stereotype.Service;
+
+@Service
+public class BookService {
+
+ private static final Logger log = LoggerFactory.getLogger(BookService.class);
+
+ private final JmsTemplate jmsTemplate;
+
+ public BookService(JmsTemplate jmsTemplate) {
+ this.jmsTemplate = jmsTemplate;
+ }
+
+ /**
+ * Scenario for "should generate tests triggered by a method": client side: must have
+ * a possibility to "trigger" sending of a message to the given messageFrom server
+ * side: will run the method and await upon receiving message on the output
+ * messageFrom. Method triggers sending a message to a source
+ */
+ @JmsListener(destination = "input2")
+ public void returnBook() {
+ BookReturned bookReturned = new BookReturned("foo");
+ jmsTemplate.convertAndSend("output2", "{\"bookName\":\"foo\"}", message -> {
+ message.setStringProperty("BOOK-NAME", bookReturned.bookName);
+ return message;
+ });
+ }
+
+}
diff --git a/tests/samples-messaging-jms/src/main/java/com/example/JmsMessagingApplication.java b/tests/samples-messaging-jms/src/main/java/com/example/JmsMessagingApplication.java
new file mode 100644
index 0000000000..d005e41c56
--- /dev/null
+++ b/tests/samples-messaging-jms/src/main/java/com/example/JmsMessagingApplication.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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 com.example;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.jms.annotation.EnableJms;
+
+@SpringBootApplication
+@EnableJms
+public class JmsMessagingApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(JmsMessagingApplication.class, args);
+ }
+
+}
diff --git a/tests/samples-messaging-jms/src/test/groovy/com/example/JmsMessagingApplicationSpec.groovy b/tests/samples-messaging-jms/src/test/groovy/com/example/JmsMessagingApplicationSpec.groovy
new file mode 100644
index 0000000000..df67ff15d8
--- /dev/null
+++ b/tests/samples-messaging-jms/src/test/groovy/com/example/JmsMessagingApplicationSpec.groovy
@@ -0,0 +1,173 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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 com.example
+
+import javax.inject.Inject
+import javax.jms.JMSException
+import javax.jms.Message
+
+import com.jayway.jsonpath.DocumentContext
+import com.jayway.jsonpath.JsonPath
+import com.toomuchcoding.jsonassert.JsonAssertion
+import org.junit.BeforeClass
+import spock.lang.Specification
+import spock.util.concurrent.PollingConditions
+
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootContextLoader
+import org.springframework.cloud.contract.spec.Contract
+import org.springframework.cloud.contract.verifier.messaging.MessageVerifier
+import org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier
+import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
+import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessaging
+import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierObjectMapper
+import org.springframework.jms.core.JmsTemplate
+import org.springframework.jms.core.MessagePostProcessor
+import org.springframework.test.annotation.DirtiesContext
+import org.springframework.test.context.ContextConfiguration
+
+/**
+ * SPIKE ON TESTS FROM NOTES IN MessagingSpec
+ */
+// Context configuration would end up in base class
+@ContextConfiguration(classes = [JmsMessagingApplication], loader = SpringBootContextLoader)
+@AutoConfigureMessageVerifier
+class JmsMessagingApplicationSpec extends Specification {
+
+ // ALL CASES
+ @Autowired
+ JmsTemplate jmsTemplate
+ @Autowired
+ BookDeleter bookDeleter
+ @Inject MessageVerifier messageVerifier
+ @Inject ContractVerifierMessaging contractVerifierMessaging
+ @Inject ContractVerifierObjectMapper contractVerifierObjectMapper
+
+ @BeforeClass
+ static void init() {
+ System.setProperty("org.apache.activemq.SERIALIZABLE_PACKAGES", "*")
+ }
+
+ def "should work for triggered based messaging"() {
+ given:
+ Contract.make {
+ label 'some_label'
+ input {
+ triggeredBy('bookReturnedTriggered()')
+ }
+ outputMessage {
+ sentTo('output')
+ body('''{ "bookName" : "foo" }''')
+ headers {
+ header('BOOK-NAME', 'foo')
+ }
+ }
+ }
+ // generated test should look like this:
+ when:
+ bookReturnedTriggered()
+ then:
+ ContractVerifierMessage response = contractVerifierMessaging.receive('output')
+ response.getHeader('BOOK-NAME') == 'foo'
+ and:
+ DocumentContext parsedJson = JsonPath.
+ parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
+ JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
+ }
+
+ @DirtiesContext
+ def "should generate tests triggered by a message"() {
+ given:
+ Contract.make {
+ label 'some_label'
+ input {
+ messageFrom('input2')
+ messageBody([
+ bookName: 'foo'
+ ])
+ messageHeaders {
+ header('sample', 'header')
+ }
+ }
+ outputMessage {
+ sentTo('output2')
+ body([
+ bookName: 'foo'
+ ])
+ headers {
+ header('BOOK-NAME', 'foo')
+ }
+ }
+ }
+ // generated test should look like this:
+ when:
+ messageVerifier.send(
+ contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
+ [sample: 'header'], 'input2')
+ then:
+ ContractVerifierMessage response = contractVerifierMessaging.receive('output2')
+ response.getHeader('BOOK-NAME') == 'foo'
+ and:
+ DocumentContext parsedJson = JsonPath.
+ parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()))
+ JsonAssertion.assertThat(parsedJson).field('bookName').isEqualTo('foo')
+ }
+
+ def "should generate tests without destination, triggered by a message"() {
+ given:
+ Contract.make {
+ label 'some_label'
+ input {
+ messageFrom('delete')
+ messageBody([
+ bookName: 'foo'
+ ])
+ messageHeaders {
+ header('sample', 'header')
+ }
+ assertThat('bookWasDeleted()')
+ }
+ }
+ // generated test should look like this:
+ when:
+ messageVerifier.
+ send(contractVerifierObjectMapper.writeValueAsString([bookName: 'foo']),
+ [sample: 'header'], 'delete')
+ then:
+ noExceptionThrown()
+ bookWasDeleted()
+ }
+
+ void bookReturnedTriggered() {
+ jmsTemplate.convertAndSend("output", '''{"bookName" : "foo" }''', new MessagePostProcessor() {
+ @Override
+ Message postProcessMessage(Message message) throws JMSException {
+ message.setStringProperty("BOOK-NAME", "foo")
+ return message
+ }
+ })
+ }
+
+ PollingConditions pollingConditions = new PollingConditions()
+
+ void bookWasDeleted() {
+ pollingConditions.eventually {
+ assert bookDeleter.bookSuccessfulyDeleted.get()
+ }
+ }
+
+}
diff --git a/tests/samples-messaging-jms/src/test/resources/application.yml b/tests/samples-messaging-jms/src/test/resources/application.yml
new file mode 100644
index 0000000000..a26da67063
--- /dev/null
+++ b/tests/samples-messaging-jms/src/test/resources/application.yml
@@ -0,0 +1,2 @@
+activemq:
+ broker-url: vm://embedded-broker?broker.persistent=false
\ No newline at end of file
diff --git a/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy b/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy
index 0f28c83194..0d456362f6 100644
--- a/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy
+++ b/tests/spring-cloud-contract-stub-runner-camel/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/camel/CamelStubRunnerSpec.groovy
@@ -76,6 +76,16 @@ class CamelStubRunnerSpec extends Specification {
this.camelContext.shutdownStrategy = strategy
}
+ def 'should not trigger a message that does not match input'() {
+ when:
+ producerTemplate.
+ sendBodyAndHeaders('jms:input', new BookReturned('notmatching'), [wrong: 'header_value'])
+ then:
+ Exchange receivedMessage = consumerTemplate.receive('jms:output', 100)
+ and:
+ receivedMessage == null
+ }
+
def 'should download the stub and register a route for it'() {
when:
// tag::client_send[]
@@ -175,16 +185,6 @@ class CamelStubRunnerSpec extends Specification {
noExceptionThrown()
}
- def 'should not trigger a message that does not match input'() {
- when:
- producerTemplate.
- sendBodyAndHeaders('jms:input', new BookReturned('notmatching'), [wrong: 'header_value'])
- then:
- Exchange receivedMessage = consumerTemplate.receive('jms:output', 100)
- and:
- receivedMessage == null
- }
-
private boolean assertThatBodyContainsBookNameFoo(Object payload) {
String objectAsString = payload instanceof String ? payload :
JsonOutput.toJson(payload)
diff --git a/tests/spring-cloud-contract-stub-runner-jms/pom.xml b/tests/spring-cloud-contract-stub-runner-jms/pom.xml
new file mode 100644
index 0000000000..12d01106b3
--- /dev/null
+++ b/tests/spring-cloud-contract-stub-runner-jms/pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+
+ org.springframework.cloud
+ spring-cloud-contract-tests
+ 2.2.0.BUILD-SNAPSHOT
+ ..
+
+ spring-cloud-contract-stub-runner-jms
+ jar
+ Spring Cloud Contract Stub Runner JMS
+ Spring Cloud Contract Stub Runner JMS
+
+
+ org.springframework.cloud
+ spring-cloud-contract-stub-runner
+
+
+ org.springframework.cloud
+ spring-cloud-starter-contract-stub-runner-jetty
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-activemq
+
+
+ junit
+ junit
+ test
+
+
+ org.codehaus.groovy
+ groovy
+
+
+ org.spockframework
+ spock-core
+ test
+
+
+ org.spockframework
+ spock-spring
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+ test
+
+
+ info.solidsoft.spock
+ spock-global-unroll
+ test
+
+
+
+
+
+ org.codehaus.gmavenplus
+ gmavenplus-plugin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+
+
diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/BookReturned.groovy b/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/BookReturned.groovy
new file mode 100644
index 0000000000..ba9c1d406f
--- /dev/null
+++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/BookReturned.groovy
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.cloud.contract.stubrunner.messaging.jms
+
+import com.fasterxml.jackson.annotation.JsonCreator
+import groovy.transform.CompileStatic
+import groovy.transform.EqualsAndHashCode
+
+@CompileStatic
+@EqualsAndHashCode
+class BookReturned implements Serializable {
+ final String bookName
+
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ BookReturned(String bookName) {
+ this.bookName = bookName
+ }
+}
diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy b/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy
new file mode 100644
index 0000000000..662b4272a6
--- /dev/null
+++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/groovy/org/springframework/cloud/contract/stubrunner/messaging/jms/JmsStubRunnerSpec.groovy
@@ -0,0 +1,274 @@
+/*
+ * Copyright 2013-2019 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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.springframework.cloud.contract.stubrunner.messaging.jms
+
+import javax.jms.JMSException
+import javax.jms.Message
+import javax.jms.TextMessage
+
+import groovy.json.JsonOutput
+import groovy.json.JsonSlurper
+import org.apache.activemq.ActiveMQConnectionFactory
+import spock.lang.IgnoreIf
+import spock.lang.Specification
+
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration
+import org.springframework.boot.test.context.SpringBootContextLoader
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.cloud.contract.spec.Contract
+import org.springframework.cloud.contract.stubrunner.StubFinder
+import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.ComponentScan
+import org.springframework.context.annotation.Configuration
+import org.springframework.jms.annotation.EnableJms
+import org.springframework.jms.core.JmsTemplate
+import org.springframework.jms.core.MessagePostProcessor
+import org.springframework.test.context.ContextConfiguration
+
+/**
+ * @author Marcin Grzejszczak
+ */
+@ContextConfiguration(classes = Config, loader = SpringBootContextLoader)
+@SpringBootTest(properties = ["debug=true"])
+@AutoConfigureStubRunner
+@IgnoreIf({ os.windows })
+class JmsStubRunnerSpec extends Specification {
+
+ @Autowired
+ StubFinder stubFinder
+ @Autowired
+ JmsTemplate jmsTemplate
+
+ def cleanup() {
+ // ensure that message were taken from the queue
+ jmsTemplate.receive('output')
+ jmsTemplate.receive('input')
+ }
+
+ def 'should download the stub and register a route for it'() {
+ when:
+ // tag::client_send[]
+ jmsTemplate.
+ convertAndSend('input', new BookReturned('foo'), new MessagePostProcessor() {
+ @Override
+ Message postProcessMessage(Message message) throws JMSException {
+ message.setStringProperty("sample", "header")
+ return message
+ }
+ })
+ // end::client_send[]
+ then:
+ // tag::client_receive[]
+ TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
+ // end::client_receive[]
+ and:
+ // tag::client_receive_message[]
+ receivedMessage != null
+ assertThatBodyContainsBookNameFoo(receivedMessage.getText())
+ receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
+ // end::client_receive_message[]
+ }
+
+ def 'should trigger a message by label'() {
+ when:
+ // tag::client_trigger[]
+ stubFinder.trigger('return_book_1')
+ // end::client_trigger[]
+ then:
+ // tag::client_trigger_receive[]
+ TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
+ // end::client_trigger_receive[]
+ and:
+ // tag::client_trigger_message[]
+ receivedMessage != null
+ assertThatBodyContainsBookNameFoo(receivedMessage.getText())
+ receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
+ // end::client_trigger_message[]
+ }
+
+ def 'should trigger a label for the existing groupId:artifactId'() {
+ when:
+ // tag::trigger_group_artifact[]
+ stubFinder.
+ trigger('my:stubs', 'return_book_1')
+ // end::trigger_group_artifact[]
+ then:
+ TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
+ and:
+ receivedMessage != null
+ assertThatBodyContainsBookNameFoo(receivedMessage.getText())
+ receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
+ }
+
+ def 'should trigger a label for the existing artifactId'() {
+ when:
+ // tag::trigger_artifact[]
+ stubFinder.trigger('stubs', 'return_book_1')
+ // end::trigger_artifact[]
+ then:
+ TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
+ and:
+ receivedMessage != null
+ assertThatBodyContainsBookNameFoo(receivedMessage.getText())
+ receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
+ }
+
+ def 'should throw an exception when missing label is passed'() {
+ when:
+ stubFinder.trigger('missing label')
+ then:
+ thrown(IllegalArgumentException)
+ }
+
+ def 'should throw an exception when missing label and artifactid is passed'() {
+ when:
+ stubFinder.trigger('some:service', 'return_book_1')
+ then:
+ thrown(IllegalArgumentException)
+ }
+
+ def 'should trigger messages by running all triggers'() {
+ when:
+ // tag::trigger_all[]
+ stubFinder.trigger()
+ // end::trigger_all[]
+ then:
+ TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
+ and:
+ receivedMessage != null
+ assertThatBodyContainsBookNameFoo(receivedMessage.getText())
+ receivedMessage.getStringProperty('BOOK-NAME') == 'foo'
+ }
+
+ def 'should trigger a label with no output message'() {
+ when:
+ // tag::trigger_no_output[]
+ jmsTemplate.
+ convertAndSend('delete', new BookReturned('foo'), new MessagePostProcessor() {
+ @Override
+ Message postProcessMessage(Message message) throws JMSException {
+ message.setStringProperty("sample", "header")
+ return message
+ }
+ })
+ // end::trigger_no_output[]
+ then:
+ noExceptionThrown()
+ }
+
+ def 'should not trigger a message that does not match input'() {
+ when:
+ jmsTemplate.
+ convertAndSend('input', new BookReturned('notmatching'), new MessagePostProcessor() {
+ @Override
+ Message postProcessMessage(Message message) throws JMSException {
+ message.setStringProperty("wrong", "header")
+ return message
+ }
+ })
+ then:
+ TextMessage receivedMessage = (TextMessage) jmsTemplate.receive('output')
+ and:
+ receivedMessage == null
+ }
+
+ private boolean assertThatBodyContainsBookNameFoo(Object payload) {
+ String objectAsString = payload instanceof String ? payload :
+ JsonOutput.toJson(payload)
+ def json = new JsonSlurper().parseText(objectAsString)
+ return json.bookName == 'foo'
+ }
+
+ @Configuration
+ @ComponentScan
+ @EnableAutoConfiguration
+ @EnableJms
+ static class Config {
+ @Bean
+ ActiveMQConnectionFactory activeMQConnectionFactory(@Value('${activemq.url:vm://localhost?broker.persistent=false}') String url) {
+ ActiveMQConnectionFactory factory = new ActiveMQConnectionFactory(brokerURL: url)
+ try {
+ factory.trustAllPackages = true
+ }
+ catch (Throwable e) {
+ }
+ return factory
+ }
+ }
+
+ Contract dsl =
+ // tag::sample_dsl[]
+ Contract.make {
+ label 'return_book_1'
+ input {
+ triggeredBy('bookReturnedTriggered()')
+ }
+ outputMessage {
+ sentTo('output')
+ body('''{ "bookName" : "foo" }''')
+ headers {
+ header('BOOK-NAME', 'foo')
+ }
+ }
+ }
+ // end::sample_dsl[]
+
+ Contract dsl2 =
+ // tag::sample_dsl_2[]
+ Contract.make {
+ label 'return_book_2'
+ input {
+ messageFrom('input')
+ messageBody([
+ bookName: 'foo'
+ ])
+ messageHeaders {
+ header('sample', 'header')
+ }
+ }
+ outputMessage {
+ sentTo('output')
+ body([
+ bookName: 'foo'
+ ])
+ headers {
+ header('BOOK-NAME', 'foo')
+ }
+ }
+ }
+ // end::sample_dsl_2[]
+
+ Contract dsl3 =
+ // tag::sample_dsl_3[]
+ Contract.make {
+ label 'delete_book'
+ input {
+ messageFrom('delete')
+ messageBody([
+ bookName: 'foo'
+ ])
+ messageHeaders {
+ header('sample', 'header')
+ }
+ assertThat('bookWasDeleted()')
+ }
+ }
+ // end::sample_dsl_3[]
+}
diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/application.yml b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/application.yml
new file mode 100644
index 0000000000..2782cdc1a5
--- /dev/null
+++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/application.yml
@@ -0,0 +1,14 @@
+stubrunner:
+ repository-root: stubs:classpath:/stubs/
+ ids: my:stubs
+ stubs-mode: remote
+spring:
+ activemq:
+ send-timeout: 1000
+ jms:
+ template:
+ receive-timeout: 1000
+server:
+ port: 0
+debug: true
+logging.level.org.springframework.cloud.contract: debug
\ No newline at end of file
diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookDeleted.groovy b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookDeleted.groovy
new file mode 100644
index 0000000000..b013bba223
--- /dev/null
+++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookDeleted.groovy
@@ -0,0 +1,13 @@
+org.springframework.cloud.contract.spec.Contract.make {
+ label 'delete_book'
+ input {
+ messageFrom('delete')
+ messageBody([
+ bookName: 'foo'
+ ])
+ messageHeaders {
+ header('sample', 'header')
+ }
+ assertThat('bookWasDeleted()')
+ }
+}
\ No newline at end of file
diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned1.groovy b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned1.groovy
new file mode 100644
index 0000000000..0094bc7b1d
--- /dev/null
+++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned1.groovy
@@ -0,0 +1,13 @@
+org.springframework.cloud.contract.spec.Contract.make {
+ label 'return_book_1'
+ input {
+ triggeredBy('bookReturnedTriggered()')
+ }
+ outputMessage {
+ sentTo('output')
+ body('''{ "bookName" : "foo" }''')
+ headers {
+ header('BOOK-NAME', 'foo')
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned2.groovy b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned2.groovy
new file mode 100644
index 0000000000..e0d53098d5
--- /dev/null
+++ b/tests/spring-cloud-contract-stub-runner-jms/src/test/resources/stubs/bookReturned2.groovy
@@ -0,0 +1,21 @@
+org.springframework.cloud.contract.spec.Contract.make {
+ label 'return_book_2'
+ input {
+ messageFrom('input')
+ messageBody([
+ bookName: 'foo'
+ ])
+ messageHeaders {
+ header('sample', 'header')
+ }
+ }
+ outputMessage {
+ sentTo('output')
+ body([
+ bookName: 'foo'
+ ])
+ headers {
+ header('BOOK-NAME', 'foo')
+ }
+ }
+}
\ No newline at end of file