From 713dd60fa74843ba8d27c1c459dc9e7beb9dac19 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Wed, 12 Mar 2014 17:43:14 +0100 Subject: [PATCH] JMS annotation-driven endpoints. This commit adds the support of JMS annotated endpoint. Can be activated both by @EnableJms or and detects methods of managed beans annotated with @JmsListener, either directly or through a meta-annotation. Containers are created and managed under the cover by a registry at application startup time. Container creation is delegated to a JmsListenerContainerFactory that is identified by the containerFactory attribute of the JmsListener annotation. Containers can be retrieved from the registry using a custom id that can be specified directly on the annotation. A "factory-id" attribute is available on the container element of the XML namespace. When it is present, the configuration defined at the namespace level is used to build a JmsListenerContainerFactory that is exposed with the value of the "factory-id" attribute. This can be used as a smooth migration path for users having listener containers defined at the namespace level. It is also possible to migrate all listeners to annotated endpoints and yet keep the or element to share the container configuration. The configuration can be fine-tuned by implementing the JmsListenerConfigurer interface which gives access to the registrar used to register endpoints. This includes a programmatic registration of endpoints in complement to the declarative approach. A default JmsListenerContainerFactory can also be specified to be used if no containerFactory has been set on the annotation. Annotated methods can have flexible method arguments that are similar to what @MessageMapping provides. In particular, jms listener endpoint methods can fully use the messaging abstraction, including convenient header accessors. It is also possible to inject the raw javax.jms.Message and the Session for more advanced use cases. The payload can be injected as long as the conversion service is able to convert it from the original type of the JMS payload. By default, a DefaultJmsHandlerMethodFactory is used but it can be configured further to support additional method arguments or to customize conversion and validation support. The return type of an annotated method can also be an instance of Spring's Message abstraction. Instead of just converting the payload, such response type allows to communicate standard and custom headers. The JmsHeaderMapper infrastructure from Spring integration has also been migrated to the Spring framework. SimpleJmsHeaderMapper is based on SI's DefaultJmsHeaderMapper. The simple implementation maps all JMS headers so that the generated Message abstraction has all the information stored in the protocol specific message. Issue: SPR-9882 --- build.gradle | 1 + .../annotation/AnnotationConfigUtils.java | 12 + .../jms/annotation/EnableJms.java | 260 ++++++++ .../annotation/JmsBootstrapConfiguration.java | 54 ++ .../jms/annotation/JmsListener.java | 122 ++++ ...msListenerAnnotationBeanPostProcessor.java | 283 +++++++++ .../jms/annotation/JmsListenerConfigurer.java | 49 ++ .../jms/annotation/package-info.java | 5 + .../AbstractJmsListenerContainerFactory.java | 173 ++++++ .../config/AbstractJmsListenerEndpoint.java | 200 ++++++ .../AbstractListenerContainerParser.java | 133 +++- ...notationDrivenJmsBeanDefinitionParser.java | 103 ++++ .../DefaultJcaListenerContainerFactory.java | 125 ++++ .../DefaultJmsHandlerMethodFactory.java | 213 +++++++ .../DefaultJmsListenerContainerFactory.java | 145 +++++ .../config/JcaListenerContainerParser.java | 112 +++- .../jms/config/JmsHandlerMethodFactory.java | 41 ++ .../config/JmsListenerContainerFactory.java | 38 ++ .../config/JmsListenerContainerParser.java | 258 +++++--- .../jms/config/JmsListenerEndpoint.java | 48 ++ .../config/JmsListenerEndpointRegistrar.java | 158 +++++ .../config/JmsListenerEndpointRegistry.java | 131 ++++ .../jms/config/JmsNamespaceHandler.java | 4 +- .../jms/config/MethodJmsListenerEndpoint.java | 125 ++++ .../SimpleJmsListenerContainerFactory.java | 36 ++ .../jms/config/SimpleJmsListenerEndpoint.java | 59 ++ .../jms/config/package-info.java | 5 +- .../AbstractMessageListenerContainer.java | 26 +- .../listener/MessageListenerContainer.java | 44 ++ .../AbstractAdaptableMessageListener.java | 413 +++++++++++++ .../adapter/MessageListenerAdapter.java | 342 +---------- .../MessagingMessageListenerAdapter.java | 104 ++++ .../endpoint/JmsActivationSpecConfig.java | 21 +- .../endpoint/JmsMessageEndpointFactory.java | 10 +- .../endpoint/JmsMessageEndpointManager.java | 44 +- .../jms/support/JmsMessageHeaderAccessor.java | 99 +++ .../support/converter/JmsHeaderMapper.java | 38 ++ .../jms/support/converter/JmsHeaders.java | 112 ++++ .../converter/SimpleJmsHeaderMapper.java | 300 +++++++++ .../main/resources/META-INF/spring.schemas | 3 +- .../jms/config/spring-jms-4.1.xsd | 575 ++++++++++++++++++ .../springframework/jms/StubTextMessage.java | 264 ++++++++ .../AbstractJmsAnnotationDrivenTests.java | 210 +++++++ .../AnnotationDrivenNamespaceTests.java | 104 ++++ .../jms/annotation/EnableJmsTests.java | 184 ++++++ ...tenerAnnotationBeanPostProcessorTests.java | 131 ++++ .../DefaultJmsHandlerMethodFactoryTests.java | 203 +++++++ ...tenerContainerFactoryIntegrationTests.java | 143 +++++ .../JmsListenerContainerFactoryTests.java | 193 ++++++ .../JmsListenerContainerTestFactory.java | 42 ++ .../JmsListenerEndpointRegistrarTests.java | 91 +++ .../JmsListenerEndpointRegistryTests.java | 69 +++ .../jms/config/JmsListenerEndpointTests.java | 103 ++++ .../jms/config/JmsNamespaceHandlerTests.java | 119 +++- .../config/MessageListenerTestContainer.java | 118 ++++ .../MethodJmsListenerEndpointTests.java | 418 +++++++++++++ .../SimpleJmsListenerEndpointTests.java | 43 ++ .../MessagingMessageListenerAdapterTests.java | 103 ++++ .../JmsMessageHeaderAccessorTests.java | 73 +++ .../converter/SimpleJmsHeaderMapperTests.java | 562 +++++++++++++++++ ...tation-driven-custom-container-factory.xml | 17 + ...n-driven-custom-handler-method-factory.xml | 23 + .../annotation-driven-custom-registry.xml | 27 + .../annotation-driven-full-config.xml | 17 + .../annotation-driven-sample-config.xml | 18 + .../jms/config/jmsNamespaceHandlerTests.xml | 19 +- .../converter/GenericMessageConverter.java | 62 ++ .../invocation/InvocableHandlerMethod.java | 6 +- .../messaging/mapping/HeaderMapper.java | 37 ++ .../messaging/mapping/package-info.java | 20 + .../GenericMessageConverterTests.java | 65 ++ 71 files changed, 7983 insertions(+), 525 deletions(-) create mode 100644 spring-jms/src/main/java/org/springframework/jms/annotation/EnableJms.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/annotation/JmsBootstrapConfiguration.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerConfigurer.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/annotation/package-info.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerEndpoint.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/AnnotationDrivenJmsBeanDefinitionParser.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/DefaultJcaListenerContainerFactory.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsHandlerMethodFactory.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/JmsHandlerMethodFactory.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerFactory.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpoint.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistry.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerContainerFactory.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerEndpoint.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/listener/MessageListenerContainer.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/support/JmsMessageHeaderAccessor.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/support/converter/JmsHeaderMapper.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/support/converter/JmsHeaders.java create mode 100644 spring-jms/src/main/java/org/springframework/jms/support/converter/SimpleJmsHeaderMapper.java create mode 100644 spring-jms/src/main/resources/org/springframework/jms/config/spring-jms-4.1.xsd create mode 100644 spring-jms/src/test/java/org/springframework/jms/StubTextMessage.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/annotation/AnnotationDrivenNamespaceTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessorTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/config/DefaultJmsHandlerMethodFactoryTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerFactoryIntegrationTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerFactoryTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointRegistrarTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointRegistryTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/config/MessageListenerTestContainer.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/config/SimpleJmsListenerEndpointTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/support/JmsMessageHeaderAccessorTests.java create mode 100644 spring-jms/src/test/java/org/springframework/jms/support/converter/SimpleJmsHeaderMapperTests.java create mode 100644 spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-container-factory.xml create mode 100644 spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-handler-method-factory.xml create mode 100644 spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-registry.xml create mode 100644 spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-full-config.xml create mode 100644 spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-sample-config.xml rename spring-jms/src/test/{java => resources}/org/springframework/jms/config/jmsNamespaceHandlerTests.xml (77%) create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/converter/GenericMessageConverter.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/mapping/HeaderMapper.java create mode 100644 spring-messaging/src/main/java/org/springframework/messaging/mapping/package-info.java create mode 100644 spring-messaging/src/test/java/org/springframework/messaging/converter/GenericMessageConverterTests.java diff --git a/build.gradle b/build.gradle index b80b4ab75c..3b4680b6b0 100644 --- a/build.gradle +++ b/build.gradle @@ -485,6 +485,7 @@ project("spring-jms") { compile(project(":spring-beans")) compile(project(":spring-aop")) compile(project(":spring-context")) + compile(project(":spring-messaging")) compile(project(":spring-tx")) provided("javax.jms:jms-api:1.1-rev-1") optional(project(":spring-oxm")) diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index 20353ae664..c876f3d704 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -190,6 +190,18 @@ public class AnnotationConfigUtils { public static final String JCACHE_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.cache.aspectj.AspectJJCacheConfiguration"; + /** + * The bean name of the internally managed jms listener annotation processor. + */ + public static final String JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME = + "org.springframework.jms.config.internalJmsListenerAnnotationProcessor"; + + /** + * The bean name of the internally managed jms listener endpoint registry. + */ + public static final String JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME = + "org.springframework.jms.config.internalJmsListenerEndpointRegistry"; + /** * The bean name of the internally managed JPA annotation processor. */ diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/EnableJms.java b/spring-jms/src/main/java/org/springframework/jms/annotation/EnableJms.java new file mode 100644 index 0000000000..d8782b2b03 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/EnableJms.java @@ -0,0 +1,260 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.annotation.Import; + +/** + * Enable JMS listener annotated endpoints that are created under the cover + * by a {@link org.springframework.jms.config.JmsListenerContainerFactory + * JmsListenerContainerFactory}. To be used on + * @{@link org.springframework.context.annotation.Configuration Configuration} classes + * as follows: + * + *
+ * @Configuration
+ * @EnableJms
+ * public class AppConfig {
+ *     @Bean
+ *     public DefaultJmsListenerContainerFactory myJmsListenerContainerFactory() {
+ *       DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
+ *       factory.setConnectionFactory(connectionFactory());
+ *       factory.setDestinationResolver(destinationResolver());
+ *       factory.setConcurrency("5");
+ *       return factory;
+ *     }
+ *     // other @Bean definitions
+ * }
+ * + * The {@code JmsListenerContainerFactory} is responsible to create the listener container + * responsible for a particular endpoint. Typical implementations, as the + * {@link org.springframework.jms.config.DefaultJmsListenerContainerFactory DefaultJmsListenerContainerFactory} + * used in the sample above, provides the necessary configuration options that are supported by + * the underlying {@link org.springframework.jms.listener.MessageListenerContainer MessageListenerContainer}. + * + *

{@code @EnableJms} enables detection of @{@link JmsListener} annotations on + * any Spring-managed bean in the container. For example, given a class {@code MyService} + * + *

+ * package com.acme.foo;
+ *
+ * public class MyService {
+ *     @JmsListener(containerFactory = "myJmsListenerContainerFactory", destination="myQueue")
+ *     public void process(String msg) {
+ *         // process incoming message
+ *     }
+ * }
+ * + * The container factory to use is identified by the {@link JmsListener#containerFactory() containerFactory} + * attribute defining the name of the {@code JmsListenerContainerFactory} bean to use. + * + *

the following configuration would ensure that every time a {@link javax.jms.Message} + * is received on the {@link javax.jms.Destination} named "myQueue", {@code MyService.process()} + * is called with the content of the message: + * + *

+ * @Configuration
+ * @EnableJms
+ * public class AppConfig {
+ *     @Bean
+ *     public MyService myService() {
+ *         return new MyService();
+ *     }
+ *
+ *     // JMS infrastructure setup
+ * }
+ * + * Alternatively, if {@code MyService} were annotated with {@code @Component}, the + * following configuration would ensure that its {@code @JmsListener} annotated + * method is invoked with a matching incoming message: + * + *
+ * @Configuration
+ * @EnableJms
+ * @ComponentScan(basePackages="com.acme.foo")
+ * public class AppConfig {
+ * }
+ * + * Note that the created containers are not registered against the application context + * but can be easily located for management purposes using the + * {@link org.springframework.jms.config.JmsListenerEndpointRegistry JmsListenerEndpointRegistry}. + * + *

Annotated methods can use flexible signature; in particular, it is possible to use + * the {@link org.springframework.messaging.Message Message} abstraction and related annotations, + * see @{@link JmsListener} Javadoc for more details. For instance, the following would + * inject the content of the message and a a custom "myCounter" JMS header: + * + *

+ * @JmsListener(containerFactory = "myJmsListenerContainerFactory", destination="myQueue")
+ * public void process(String msg, @Header("myCounter") int counter) {
+ *     // process incoming message
+ * }
+ * + * These features are abstracted by the {@link org.springframework.jms.config.JmsHandlerMethodFactory + * JmsHandlerMethodFactory} that is responsible to build the necessary invoker to process + * the annotated method. By default, {@link org.springframework.jms.config.DefaultJmsHandlerMethodFactory + * DefaultJmsHandlerMethodFactory} is used. + * + *

When more control is desired, a {@code @Configuration} class may implement + * {@link JmsListenerConfigurer}. This allows access to the underlying + * {@link org.springframework.jms.config.JmsListenerEndpointRegistrar JmsListenerEndpointRegistrar} + * instance. The following example demonstrates how to specify a default + * {@code JmsListenerContainerFactory} so that {@link JmsListener#containerFactory()} may be + * omitted for endpoints willing to use the default container factory. + * + *

+ * @Configuration
+ * @EnableJms
+ * public class AppConfig implements JmsListenerConfigurer {
+ *     @Override
+ *     public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
+ *         registrar.setDefaultContainerFactory(myJmsListenerContainerFactory());
+ *     }
+ *
+ *     @Bean
+ *     public JmsListenerContainerFactory myJmsListenerContainerFactory() {
+ *         // factory settings
+ *     }
+ *
+ *     @Bean
+ *     public MyService myService() {
+ *         return new MyService();
+ *     }
+ * }
+ * + * For reference, the example above can be compared to the following Spring XML + * configuration: + *
+ * {@code 
+ *     
+ *
+ *     
+ *           // factory settings
+ *     
+ *
+ *     
+ * 
+ * }
+ * + * It is also possible to specify a custom {@link org.springframework.jms.config.JmsListenerEndpointRegistry + * JmsListenerEndpointRegistry} in case you need more control on the way the containers + * are created and managed. The example below also demonstrates how to customize the + * {@code JmsHandlerMethodFactory} to use with a custom {@link org.springframework.validation.Validator + * Validator} so that payloads annotated with {@link org.springframework.validation.annotation.Validated + * @Validated} are first validated against a custom {@code Validator}. + * + *
+ * @Configuration
+ * @EnableJms
+ * public class AppConfig implements JmsListenerConfigurer {
+ *     @Override
+ *     public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
+ *         registrar.setEndpointRegistry(myJmsListenerEndpointRegistry());
+ *         registrar.setJmsHandlerMethodFactory(myJmsHandlerMethodFactory);
+ *     }
+ *
+ *     @Bean
+ *     public JmsListenerEndpointRegistry myJmsListenerEndpointRegistry() {
+ *         // registry configuration
+ *     }
+ *
+ *     @Bean
+ *     public JmsHandlerMethodFactory myJmsHandlerMethodFactory() {
+ *        DefaultJmsHandlerMethodFactory factory = new DefaultJmsHandlerMethodFactory();
+ *        factory.setValidator(new MyValidator());
+ *        return factory;
+ *     }
+ *
+ *     @Bean
+ *     public MyService myService() {
+ *         return new MyService();
+ *     }
+ * }
+ * + * For reference, the example above can be compared to the following Spring XML + * configuration: + *
+ * {@code 
+ *     
+ *           // registry configuration
+ *     
+ *
+ *     
+ *         
+ *     
+ *
+ *     
+ * 
+ * }
+ * + * Implementing {@code JmsListenerConfigurer} also allows for fine-grained + * control over endpoints registration via the {@code JmsListenerEndpointRegistrar}. + * For example, the following configures an extra endpoint: + * + *
+ * @Configuration
+ * @EnableJms
+ * public class AppConfig implements JmsListenerConfigurer {
+ *     @Override
+ *     public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
+ *         SimpleJmsListenerEndpoint myEndpoint = new SimpleJmsListenerEndpoint();
+ *         // ... configure the endpoint
+ *         registrar.registerEndpoint(endpoint, anotherJmsListenerContainerFactory());
+ *     }
+ *
+ *     @Bean
+ *     public MyService myService() {
+ *         return new MyService();
+ *     }
+ *
+ *     @Bean
+ *     public JmsListenerContainerFactory anotherJmsListenerContainerFactory() {
+ *         // ...
+ *     }
+ *
+ *     // JMS infrastructure setup
+ * }
+ * + * Note that all beans implementing {@code JmsListenerConfigurer} will be detected and + * invoked in a similar fashion. The example above can be translated in a regular bean + * definition registered in the context in case you use the XML configuration. + * + * @author Stephane Nicoll + * @since 4.1 + * @see JmsListener + * @see JmsListenerAnnotationBeanPostProcessor + * @see org.springframework.jms.config.JmsListenerEndpointRegistrar + * @see org.springframework.jms.config.JmsListenerEndpointRegistry + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Import(JmsBootstrapConfiguration.class) +public @interface EnableJms { +} diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsBootstrapConfiguration.java b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsBootstrapConfiguration.java new file mode 100644 index 0000000000..4cacee7aa7 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsBootstrapConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.annotation; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.jms.config.JmsListenerEndpointRegistry; + +/** + * {@code @Configuration} class that registers a {@link JmsListenerAnnotationBeanPostProcessor} + * bean capable of processing Spring's @{@link JmsListener} annotation. Also register + * a default {@link JmsListenerEndpointRegistry}. + * + *

This configuration class is automatically imported when using the @{@link EnableJms} + * annotation. See {@link EnableJms} Javadoc for complete usage. + * + * @author Stephane Nicoll + * @since 4.1 + * @see JmsListenerAnnotationBeanPostProcessor + * @see JmsListenerEndpointRegistry + * @see EnableJms + */ +@Configuration +public class JmsBootstrapConfiguration { + + @Bean(name = AnnotationConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public JmsListenerAnnotationBeanPostProcessor jmsListenerAnnotationProcessor() { + return new JmsListenerAnnotationBeanPostProcessor(); + } + + @Bean(name = AnnotationConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME) + public JmsListenerEndpointRegistry defaultJmsListenerEndpointRegistry() { + return new JmsListenerEndpointRegistry(); + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java new file mode 100644 index 0000000000..c476c68876 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListener.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.messaging.handler.annotation.MessageMapping; + +/** + * Annotation that marks a method to be the target of a JMS message + * listener on the specified {@link #destination()}. The {@link #containerFactory()} + * identifies the {@link org.springframework.jms.config.JmsListenerContainerFactory + * JmsListenerContainerFactory} to use to build the jms listener container. It may + * be omitted as long as a default container factory has been defined. + * + *

Processing of {@code @JmsListener} annotations is performed by + * registering a {@link JmsListenerAnnotationBeanPostProcessor}. This can be + * done manually or, more conveniently, through the {@code } + * element or {@link EnableJms} annotation. + * + *

Annotated methods are allowed to have flexible signatures similar to what + * {@link MessageMapping} provides, that is + *

    + *
  • {@link javax.jms.Session} to get access to the JMS session
  • + *
  • {@link javax.jms.Message} or one if subclass to get access to the raw JMS message
  • + *
  • {@link org.springframework.messaging.Message} to use the messaging abstraction counterpart
  • + *
  • {@link org.springframework.messaging.handler.annotation.Payload @Payload}-annotated method + * arguments including the support of validation
  • + *
  • {@link org.springframework.messaging.handler.annotation.Header @Header}-annotated method + * arguments to extract a specific header value, including standard JMS headers defined by + * {@link org.springframework.jms.support.converter.JmsHeaders JmsHeaders}
  • + *
  • {@link org.springframework.messaging.handler.annotation.Headers @Headers}-annotated + * argument that must also be assignable to {@link java.util.Map} for getting access to all + * headers.
  • + *
  • {@link org.springframework.messaging.MessageHeaders MessageHeaders} arguments for + * getting access to all headers.
  • + *
  • {@link org.springframework.messaging.support.MessageHeaderAccessor MessageHeaderAccessor} + * or {@link org.springframework.jms.support.JmsMessageHeaderAccessor JmsMessageHeaderAccessor} + * for convenient access to all method arguments.
  • + *
+ * + *

Annotated method may have a non {@code void} return type. When they do, the result of the + * method invocation is sent as a JMS reply to the destination defined by either the + * {@code JMSReplyTO} header of the incoming message or the value of {@link #responseDestination()}. + * + * @author Stephane Nicoll + * @since 4.1 + * @see EnableJms + * @see JmsListenerAnnotationBeanPostProcessor + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@MessageMapping +@Documented +public @interface JmsListener { + + /** + * The unique identifier of the container managing this endpoint. + *

if none is specified an auto-generated one is provided. + * @see org.springframework.jms.config.JmsListenerEndpointRegistry#getContainer(String) + */ + String id() default ""; + + /** + * The bean name of the {@link org.springframework.jms.config.JmsListenerContainerFactory} + * to use to create the message listener container responsible to serve this endpoint. + *

If not specified, the default container factory is used, if any. + */ + String containerFactory() default ""; + + /** + * The destination name for this listener, resolved through the container-wide + * {@link org.springframework.jms.support.destination.DestinationResolver} strategy. + */ + String destination(); + + /** + * Specify if the {@link #destination()} refers to a queue or not. Refer to a + * queue by default. + */ + boolean queue() default true; + + /** + * The name for the durable subscription, if any. + */ + String subscription() default ""; + + /** + * The JMS message selector expression, if any + *

See the JMS specification for a detailed definition of selector expressions. + */ + String selector() default ""; + + /** + * The name of the default response destination to send response messages to. + *

This will be applied in case of a request message that does not carry + * a "JMSReplyTo" field. The type of this destination will be determined + * by the listener-container's "destination-type" attribute. + *

Note: This only applies to a listener method with a return value, + * for which each result object will be converted into a response message. + */ + String responseDestination() default ""; + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java new file mode 100644 index 0000000000..784a5762fd --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java @@ -0,0 +1,283 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.annotation; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.jms.config.DefaultJmsHandlerMethodFactory; +import org.springframework.jms.config.JmsHandlerMethodFactory; +import org.springframework.jms.config.JmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerEndpointRegistrar; +import org.springframework.jms.config.JmsListenerEndpointRegistry; +import org.springframework.jms.config.MethodJmsListenerEndpoint; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +/** + * Bean post-processor that registers methods annotated with @{@link JmsListener} + * to be invoked by a JMS message listener container created under the cover + * by a {@link org.springframework.jms.config.JmsListenerContainerFactory} according + * to the parameters of the annotation. + * + *

Annotated methods can use flexible arguments as defined by @{@link JmsListener}. + * + *

This post-processor is automatically registered by Spring's + * {@code } XML element, and also by the @{@link EnableJms} + * annotation. + * + *

Auto-detect any {@link JmsListenerConfigurer} instances in the container, + * allowing for customization of the registry to be used, the default container + * factory or for fine-grained control over endpoints registration. See + * @{@link EnableJms} Javadoc for complete usage details. + * + * @author Stephane Nicoll + * @since 4.1 + * @see JmsListener + * @see EnableJms + * @see JmsListenerConfigurer + * @see JmsListenerEndpointRegistrar + * @see JmsListenerEndpointRegistry + * @see org.springframework.jms.config.AbstractJmsListenerEndpoint + * @see MethodJmsListenerEndpoint + * @see MessageListenerFactory + */ +public class JmsListenerAnnotationBeanPostProcessor implements BeanPostProcessor, Ordered, + ApplicationContextAware, ApplicationListener { + + private final AtomicInteger counter = new AtomicInteger(); + + private ApplicationContext applicationContext; + + private JmsListenerEndpointRegistry endpointRegistry; + + private JmsListenerContainerFactory defaultContainerFactory; + + private final JmsHandlerMethodFactoryAdapter jmsHandlerMethodFactory = new JmsHandlerMethodFactoryAdapter(); + + private final JmsListenerEndpointRegistrar registrar = new JmsListenerEndpointRegistrar(); + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** + * Set the {@link JmsListenerEndpointRegistry} that will hold the created + * endpoint and manage the lifecycle of the related listener container. + */ + public void setEndpointRegistry(JmsListenerEndpointRegistry endpointRegistry) { + this.endpointRegistry = endpointRegistry; + } + + /** + * Set the default {@link JmsListenerContainerFactory} to use in case a + * {@link JmsListener} does not define any. + * {@linkplain JmsListener#containerFactory() containerFactory} + */ + public void setDefaultContainerFactory(JmsListenerContainerFactory defaultContainerFactory) { + this.defaultContainerFactory = defaultContainerFactory; + } + + /** + * Set the {@link JmsHandlerMethodFactory} to use to configure the message + * listener responsible to serve an endpoint detected by this processor. + *

By default, {@link DefaultJmsHandlerMethodFactory} is used and it + * can be configured further to support additional method arguments + * or to customize conversion and validation support. See + * {@link DefaultJmsHandlerMethodFactory} Javadoc for more details. + */ + public void setJmsHandlerMethodFactory(JmsHandlerMethodFactory jmsHandlerMethodFactory) { + this.jmsHandlerMethodFactory.setJmsHandlerMethodFactory(jmsHandlerMethodFactory); + } + + @Override + public int getOrder() { + return LOWEST_PRECEDENCE; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(final Object bean, String beanName) throws BeansException { + Class targetClass = AopUtils.getTargetClass(bean); + ReflectionUtils.doWithMethods(targetClass, new ReflectionUtils.MethodCallback() { + @Override + public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { + JmsListener jmsListener = AnnotationUtils.getAnnotation(method, JmsListener.class); + if (jmsListener != null) { + processJmsListener(jmsListener, method, bean); + } + } + }); + return bean; + } + + protected void processJmsListener(JmsListener jmsListener, Method method, Object bean) { + if (AopUtils.isJdkDynamicProxy(bean)) { + try { + // Found a @JmsListener method on the target class for this JDK proxy -> + // is it also present on the proxy itself? + method = bean.getClass().getMethod(method.getName(), method.getParameterTypes()); + } + catch (SecurityException ex) { + ReflectionUtils.handleReflectionException(ex); + } + catch (NoSuchMethodException ex) { + throw new IllegalStateException(String.format( + "@JmsListener method '%s' found on bean target class '%s', " + + "but not found in any interface(s) for bean JDK proxy. Either " + + "pull the method up to an interface or switch to subclass (CGLIB) " + + "proxies by setting proxy-target-class/proxyTargetClass " + + "attribute to 'true'", method.getName(), method.getDeclaringClass().getSimpleName())); + } + } + + MethodJmsListenerEndpoint endpoint = new MethodJmsListenerEndpoint(); + endpoint.setBean(bean); + endpoint.setMethod(method); + endpoint.setJmsHandlerMethodFactory(jmsHandlerMethodFactory); + endpoint.setId(getEndpointId(jmsListener)); + endpoint.setDestination(jmsListener.destination()); + endpoint.setQueue(jmsListener.queue()); + if (StringUtils.hasText(jmsListener.selector())) { + endpoint.setSelector(jmsListener.selector()); + } + if (StringUtils.hasText(jmsListener.subscription())) { + endpoint.setSubscription(jmsListener.subscription()); + } + if (StringUtils.hasText(jmsListener.responseDestination())) { + endpoint.setResponseDestination(jmsListener.responseDestination()); + } + + JmsListenerContainerFactory factory = null; + String containerFactoryBeanName = jmsListener.containerFactory(); + if (StringUtils.hasText(containerFactoryBeanName)) { + try { + factory = applicationContext.getBean(containerFactoryBeanName, JmsListenerContainerFactory.class); + } + catch (NoSuchBeanDefinitionException e) { + throw new BeanInitializationException("Could not register jms listener endpoint on [" + + method + "], no " + JmsListenerContainerFactory.class.getSimpleName() + " with id '" + + containerFactoryBeanName + "' was found in the application context", e); + } + } + + registrar.registerEndpoint(endpoint, factory); + + } + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + if (event.getApplicationContext() != this.applicationContext) { + return; + } + + Map instances = + this.applicationContext.getBeansOfType(JmsListenerConfigurer.class); + for (JmsListenerConfigurer configurer : instances.values()) { + configurer.configureJmsListeners(registrar); + } + if (registrar.getEndpointRegistry() == null) { + if (endpointRegistry == null) { + endpointRegistry = applicationContext + .getBean(AnnotationConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME, + JmsListenerEndpointRegistry.class); + } + registrar.setEndpointRegistry(endpointRegistry); + } + if (registrar.getDefaultContainerFactory() == null && defaultContainerFactory != null) { + registrar.setDefaultContainerFactory(defaultContainerFactory); + } + + JmsHandlerMethodFactory handlerMethodFactory = registrar.getJmsHandlerMethodFactory(); + if (handlerMethodFactory != null) { + this.jmsHandlerMethodFactory.setJmsHandlerMethodFactory(handlerMethodFactory); + } + + // Create all the listeners and starts them + try { + registrar.afterPropertiesSet(); + } + catch (Exception e) { + throw new BeanInitializationException(e.getMessage(), e); + } + } + + + private String getEndpointId(JmsListener jmsListener) { + if (StringUtils.hasText(jmsListener.id())) { + return jmsListener.id(); + } + else { + return "org.springframework.jms.JmsListenerEndpointContainer#" + + counter.getAndIncrement(); + } + } + + /** + * An {@link JmsHandlerMethodFactory} adapter that offers a configurable underlying + * instance to use. Useful if the factory to use is determined once the endpoints + * have been registered but not created yet. + * @see JmsListenerEndpointRegistrar#setJmsHandlerMethodFactory(JmsHandlerMethodFactory) + */ + private class JmsHandlerMethodFactoryAdapter implements JmsHandlerMethodFactory { + + private JmsHandlerMethodFactory jmsHandlerMethodFactory; + + private void setJmsHandlerMethodFactory(JmsHandlerMethodFactory jmsHandlerMethodFactory) { + this.jmsHandlerMethodFactory = jmsHandlerMethodFactory; + } + + @Override + public InvocableHandlerMethod createInvocableHandlerMethod(Object bean, Method method) { + return getJmsHandlerMethodFactory().createInvocableHandlerMethod(bean, method); + } + + private JmsHandlerMethodFactory getJmsHandlerMethodFactory() { + if (jmsHandlerMethodFactory == null) { + jmsHandlerMethodFactory= createDefaultJmsHandlerMethodFactory(); + } + return jmsHandlerMethodFactory; + } + + private JmsHandlerMethodFactory createDefaultJmsHandlerMethodFactory() { + DefaultJmsHandlerMethodFactory defaultFactory = new DefaultJmsHandlerMethodFactory(); + defaultFactory.setApplicationContext(applicationContext); + defaultFactory.afterPropertiesSet(); + return defaultFactory; + } + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerConfigurer.java b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerConfigurer.java new file mode 100644 index 0000000000..09f527d87b --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerConfigurer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.annotation; + +import org.springframework.jms.config.JmsListenerEndpointRegistrar; + +/** + * Optional interface to be implemented by Spring managed bean willing + * to customize how JMS listener endpoints are configured. Typically + * used to defined the default {@link org.springframework.jms.config.JmsListenerContainerFactory + * JmsListenerContainerFactory} to use or for registering jms endpoints + * in a programmatic fashion as opposed to the declarative + * approach of using the @{@link JmsListener} annotation. + * + *

See @{@link EnableJms} for detailed usage examples. + * + * @author Stephane Nicoll + * @since 4.1 + * @see EnableJms + * @see JmsListenerEndpointRegistrar + */ +public interface JmsListenerConfigurer { + + /** + * Callback allowing a {@link org.springframework.jms.config.JmsListenerEndpointRegistry + * JmsListenerEndpointRegistry} and specific {@link org.springframework.jms.config.AbstractJmsListenerEndpoint + * JmsListenerEndpoint} instances to be registered against the given + * {@link JmsListenerEndpointRegistrar}. The default + * {@link org.springframework.jms.config.JmsListenerContainerFactory JmsListenerContainerFactory} + * can also be customized. + * @param registrar the registrar to be configured + */ + void configureJmsListeners(JmsListenerEndpointRegistrar registrar); + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/package-info.java b/spring-jms/src/main/java/org/springframework/jms/annotation/package-info.java new file mode 100644 index 0000000000..6226e5cad8 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/package-info.java @@ -0,0 +1,5 @@ +/** + * Annotations and supporting classes for declarative jms listener + * endpoint + */ +package org.springframework.jms.annotation; \ No newline at end of file diff --git a/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java new file mode 100644 index 0000000000..f5344afe84 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + + +import javax.jms.ConnectionFactory; + +import org.springframework.jms.listener.AbstractMessageListenerContainer; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.jms.support.destination.DestinationResolver; +import org.springframework.util.ErrorHandler; + +/** + * Base {@link JmsListenerContainerFactory} for Spring's base container implementation. + * + * @author Stephane Nicoll + * @since 4.1 + * @see AbstractMessageListenerContainer + */ +public abstract class AbstractJmsListenerContainerFactory + implements JmsListenerContainerFactory { + + private ConnectionFactory connectionFactory; + + private DestinationResolver destinationResolver; + + private ErrorHandler errorHandler; + + private MessageConverter messageConverter; + + private Boolean sessionTransacted; + + private Integer sessionAcknowledgeMode; + + private Boolean pubSubDomain; + + private Boolean subscriptionDurable; + + private String clientId; + + /** + * @see AbstractMessageListenerContainer#setConnectionFactory(ConnectionFactory) + */ + public void setConnectionFactory(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + /** + * @see AbstractMessageListenerContainer#setDestinationResolver(DestinationResolver) + */ + public void setDestinationResolver(DestinationResolver destinationResolver) { + this.destinationResolver = destinationResolver; + } + + /** + * @see AbstractMessageListenerContainer#setErrorHandler(ErrorHandler) + */ + public void setErrorHandler(ErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + /** + * @see AbstractMessageListenerContainer#setMessageConverter(MessageConverter) + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + /** + * @see AbstractMessageListenerContainer#setSessionTransacted(boolean) + */ + public void setSessionTransacted(Boolean sessionTransacted) { + this.sessionTransacted = sessionTransacted; + } + + /** + * @see AbstractMessageListenerContainer#setSessionAcknowledgeMode(int) + */ + public void setSessionAcknowledgeMode(Integer sessionAcknowledgeMode) { + this.sessionAcknowledgeMode = sessionAcknowledgeMode; + } + + /** + * @see AbstractMessageListenerContainer#setPubSubDomain(boolean) + */ + public void setPubSubDomain(Boolean pubSubDomain) { + this.pubSubDomain = pubSubDomain; + } + + /** + * @see AbstractMessageListenerContainer#setSubscriptionDurable(boolean) + */ + public void setSubscriptionDurable(Boolean subscriptionDurable) { + this.subscriptionDurable = subscriptionDurable; + } + + /** + * @see AbstractMessageListenerContainer#setClientId(String) + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * Create an empty container instance. + */ + protected abstract C createContainerInstance(); + + @Override + public C createMessageListenerContainer(JmsListenerEndpoint endpoint) { + C instance = createContainerInstance(); + + if (this.connectionFactory != null) { + instance.setConnectionFactory(this.connectionFactory); + } + if (this.destinationResolver != null) { + instance.setDestinationResolver(this.destinationResolver); + } + if (this.errorHandler != null) { + instance.setErrorHandler(this.errorHandler); + } + if (this.messageConverter != null) { + instance.setMessageConverter(this.messageConverter); + } + + if (this.sessionTransacted != null) { + instance.setSessionTransacted(this.sessionTransacted); + } + if (this.sessionAcknowledgeMode != null) { + instance.setSessionAcknowledgeMode(this.sessionAcknowledgeMode); + } + + if (this.pubSubDomain != null) { + instance.setPubSubDomain(this.pubSubDomain); + } + if (this.subscriptionDurable != null) { + instance.setSubscriptionDurable(this.subscriptionDurable); + } + if (this.clientId != null) { + instance.setClientId(this.clientId); + } + + endpoint.setupMessageContainer(instance); + + initializeContainer(instance); + + return instance; + } + + /** + * Further initialize the specified container. + *

Subclasses can inherit from this method to apply extra + * configuration if necessary. + */ + protected void initializeContainer(C instance) { + + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerEndpoint.java b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerEndpoint.java new file mode 100644 index 0000000000..4f48158d5d --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerEndpoint.java @@ -0,0 +1,200 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import javax.jms.MessageListener; + +import org.springframework.jms.listener.AbstractMessageListenerContainer; +import org.springframework.jms.listener.MessageListenerContainer; +import org.springframework.jms.listener.endpoint.JmsActivationSpecConfig; +import org.springframework.jms.listener.endpoint.JmsMessageEndpointManager; +import org.springframework.util.Assert; + +/** + * Base model for a JMS listener endpoint + * + * @author Stephane Nicoll + * @since 4.1 + * @see MethodJmsListenerEndpoint + * @see SimpleJmsListenerEndpoint + */ +public abstract class AbstractJmsListenerEndpoint implements JmsListenerEndpoint { + + private String id; + + private String destination; + + private boolean queue = true; + + private String subscription; + + private String selector; + + + @Override + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + /** + * Return the name of the destination for this endpoint. + */ + public String getDestination() { + return destination; + } + + /** + * Set the name of the destination for this endpoint. + */ + public void setDestination(String destination) { + this.destination = destination; + } + + /** + * Return {@code true} if the destination is a queue. + */ + public boolean isQueue() { + return queue; + } + + /** + * Specify if the destination is a queue. + */ + public void setQueue(boolean queue) { + this.queue = queue; + } + + /** + * Return the name for the durable subscription, if any. + */ + public String getSubscription() { + return subscription; + } + + /** + * Set the name for the durable subscription. + */ + public void setSubscription(String subscription) { + this.subscription = subscription; + } + + /** + * Return the JMS message selector expression, if any. + *

See the JMS specification for a detailed definition of selector expressions. + */ + public String getSelector() { + return selector; + } + + /** + * Set the JMS message selector expression. + */ + public void setSelector(String selector) { + this.selector = selector; + } + + @Override + public void setupMessageContainer(MessageListenerContainer container) { + if (container instanceof AbstractMessageListenerContainer) { // JMS + setupJmsMessageContainer((AbstractMessageListenerContainer) container); + } + else if (container instanceof JmsMessageEndpointManager) { // JCA + setupJcaMessageContainer((JmsMessageEndpointManager) container); + } + else { + throw new IllegalArgumentException("Could not configure endpoint with the specified container '" + + container + "' Only JMS (" + AbstractMessageListenerContainer.class.getName() + + " subclass) or JCA (" + JmsMessageEndpointManager.class.getName() + ") are supported."); + } + } + + protected void setupJmsMessageContainer(AbstractMessageListenerContainer container) { + if (getDestination() != null) { + container.setDestinationName(getDestination()); + } + if (getSubscription() != null) { + container.setDurableSubscriptionName(getSubscription()); + } + if (getSelector() != null) { + container.setMessageSelector(getSelector()); + } + container.setPubSubDomain(!isQueue()); + setupMessageListener(container); + } + + protected void setupJcaMessageContainer(JmsMessageEndpointManager container) { + JmsActivationSpecConfig activationSpecConfig = container.getActivationSpecConfig(); + if (activationSpecConfig == null) { + activationSpecConfig = new JmsActivationSpecConfig(); + container.setActivationSpecConfig(activationSpecConfig); + } + + if (getDestination() != null) { + activationSpecConfig.setDestinationName(getDestination()); + } + if (getSubscription() != null) { + activationSpecConfig.setDurableSubscriptionName(getSubscription()); + } + if (getSelector() != null) { + activationSpecConfig.setMessageSelector(getSelector()); + } + activationSpecConfig.setPubSubDomain(!isQueue()); + setupMessageListener(container); + } + + /** + * Create a {@link MessageListener} that is able to serve this endpoint for the + * specified container. + */ + protected abstract MessageListener createMessageListener(MessageListenerContainer container); + + private void setupMessageListener(MessageListenerContainer container) { + MessageListener messageListener = createMessageListener(container); + Assert.state(messageListener != null, "Endpoint [" + this + "] must provide a non null message listener"); + container.setupMessageListener(messageListener); + } + + /** + * Return a description for this endpoint. + *

Available to subclasses, for inclusion in their {@code toString()} result. + */ + protected StringBuilder getEndpointDescription() { + StringBuilder result = new StringBuilder(); + return result.append(getClass().getSimpleName()) + .append("[") + .append(this.id) + .append("] destination=") + .append(this.destination) + .append(" | queue='") + .append(this.queue) + .append("' | subscription='") + .append(this.subscription) + .append(" | selector='") + .append(this.selector) + .append("'"); + } + + @Override + public String toString() { + return getEndpointDescription().toString(); + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/AbstractListenerContainerParser.java b/spring-jms/src/main/java/org/springframework/jms/config/AbstractListenerContainerParser.java index c60f7b6cc0..ccb23c68c4 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/AbstractListenerContainerParser.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/AbstractListenerContainerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -22,6 +22,9 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; @@ -36,10 +39,13 @@ import org.springframework.util.StringUtils; * common properties that are identical for all listener container variants. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.5 */ abstract class AbstractListenerContainerParser implements BeanDefinitionParser { + protected static final String FACTORY_ID_ATTRIBUTE = "factory-id"; + protected static final String LISTENER_ELEMENT = "listener"; protected static final String ID_ATTRIBUTE = "id"; @@ -95,13 +101,21 @@ abstract class AbstractListenerContainerParser implements BeanDefinitionParser { new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element)); parserContext.pushContainingComponent(compositeDef); + // First parse the common configuration of the container + PropertyValues containerValues = parseProperties(element, parserContext); + + // Expose the factory if requested + parseContainerFactory(element, parserContext, containerValues); + NodeList childNodes = element.getChildNodes(); for (int i = 0; i < childNodes.getLength(); i++) { Node child = childNodes.item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { String localName = parserContext.getDelegate().getLocalName(child); if (LISTENER_ELEMENT.equals(localName)) { - parseListener((Element) child, element, parserContext); + ListenerContainerParserContext context = new ListenerContainerParserContext( + element, (Element) child, containerValues, parserContext); + parseListener(context); } } } @@ -110,10 +124,46 @@ abstract class AbstractListenerContainerParser implements BeanDefinitionParser { return null; } - private void parseListener(Element listenerEle, Element containerEle, ParserContext parserContext) { + /** + * Parse the common properties for all listeners as defined by the specified + * container {@link Element}. + */ + protected abstract PropertyValues parseProperties(Element containerEle, ParserContext parserContext); + + /** + * Create the {@link BeanDefinition} for the container factory using the specified + * shared property values. + */ + protected abstract RootBeanDefinition createContainerFactory(String factoryId, + Element containerElement, PropertyValues propertyValues); + + /** + * Create the container {@link BeanDefinition} for the specified context. + */ + protected abstract BeanDefinition createContainer(ListenerContainerParserContext context); + + + private void parseContainerFactory(Element element, ParserContext parserContext, PropertyValues propertyValues) { + String factoryId = element.getAttribute(FACTORY_ID_ATTRIBUTE); + if (StringUtils.hasText(factoryId)) { + RootBeanDefinition beanDefinition = createContainerFactory(factoryId, element, propertyValues); + if (beanDefinition != null) { + beanDefinition.setSource(parserContext.extractSource(element)); + parserContext.registerBeanComponent(new BeanComponentDefinition(beanDefinition, factoryId)); + } + } + } + + private void parseListener(ListenerContainerParserContext context) { + ParserContext parserContext = context.getParserContext(); + PropertyValues containerValues = context.getContainerValues(); + Element listenerEle = context.getListenerElement(); + RootBeanDefinition listenerDef = new RootBeanDefinition(); listenerDef.setSource(parserContext.extractSource(listenerEle)); + BeanDefinition containerDef = createContainer(context); + String ref = listenerEle.getAttribute(REF_ATTRIBUTE); if (!StringUtils.hasText(ref)) { parserContext.getReaderContext().error( @@ -133,23 +183,15 @@ abstract class AbstractListenerContainerParser implements BeanDefinitionParser { } listenerDef.getPropertyValues().add("defaultListenerMethod", method); - if (containerEle.hasAttribute(MESSAGE_CONVERTER_ATTRIBUTE)) { - String messageConverter = containerEle.getAttribute(MESSAGE_CONVERTER_ATTRIBUTE); - if (!StringUtils.hasText(messageConverter)) { - parserContext.getReaderContext().error( - "Listener container 'message-converter' attribute contains empty value.", containerEle); - } - else { - listenerDef.getPropertyValues().add("messageConverter", - new RuntimeBeanReference(messageConverter)); - } + PropertyValue messageConverterPv = getMessageConverter(containerValues); + if (messageConverterPv != null) { + listenerDef.getPropertyValues().addPropertyValue(messageConverterPv); } - BeanDefinition containerDef = parseContainer(listenerEle, containerEle, parserContext); if (listenerEle.hasAttribute(RESPONSE_DESTINATION_ATTRIBUTE)) { String responseDestination = listenerEle.getAttribute(RESPONSE_DESTINATION_ATTRIBUTE); - boolean pubSubDomain = indicatesPubSub(containerDef); + boolean pubSubDomain = indicatesPubSub(containerValues); listenerDef.getPropertyValues().add( pubSubDomain ? "defaultResponseTopicName" : "defaultResponseQueueName", responseDestination); if (containerDef.getPropertyValues().contains("destinationResolver")) { @@ -172,13 +214,14 @@ abstract class AbstractListenerContainerParser implements BeanDefinitionParser { parserContext.registerBeanComponent(new BeanComponentDefinition(containerDef, containerBeanName)); } - protected abstract BeanDefinition parseContainer( - Element listenerEle, Element containerEle, ParserContext parserContext); - - protected boolean indicatesPubSub(BeanDefinition containerDef) { + protected boolean indicatesPubSub(PropertyValues propertyValues) { return false; } + protected PropertyValue getMessageConverter(PropertyValues containerValues) { + return containerValues.getPropertyValue("messageConverter"); + } + protected void parseListenerConfiguration(Element ele, ParserContext parserContext, BeanDefinition configDef) { String destination = ele.getAttribute(DESTINATION_ATTRIBUTE); if (!StringUtils.hasText(destination)) { @@ -206,7 +249,9 @@ abstract class AbstractListenerContainerParser implements BeanDefinitionParser { } } - protected void parseContainerConfiguration(Element ele, ParserContext parserContext, BeanDefinition configDef) { + protected PropertyValues parseCommonContainerProperties(Element ele, ParserContext parserContext) { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + String destinationType = ele.getAttribute(DESTINATION_TYPE_ATTRIBUTE); boolean pubSubDomain = false; boolean subscriptionDurable = false; @@ -224,8 +269,8 @@ abstract class AbstractListenerContainerParser implements BeanDefinitionParser { parserContext.getReaderContext().error("Invalid listener container 'destination-type': " + "only \"queue\", \"topic\" and \"durableTopic\" supported.", ele); } - configDef.getPropertyValues().add("pubSubDomain", pubSubDomain); - configDef.getPropertyValues().add("subscriptionDurable", subscriptionDurable); + propertyValues.add("pubSubDomain", pubSubDomain); + propertyValues.add("subscriptionDurable", subscriptionDurable); if (ele.hasAttribute(CLIENT_ID_ATTRIBUTE)) { String clientId = ele.getAttribute(CLIENT_ID_ATTRIBUTE); @@ -233,8 +278,9 @@ abstract class AbstractListenerContainerParser implements BeanDefinitionParser { parserContext.getReaderContext().error( "Listener 'client-id' attribute contains empty value.", ele); } - configDef.getPropertyValues().add("clientId", clientId); + propertyValues.add("clientId", clientId); } + return propertyValues; } protected Integer parseAcknowledgeMode(Element ele, ParserContext parserContext) { @@ -261,8 +307,45 @@ abstract class AbstractListenerContainerParser implements BeanDefinitionParser { } } - protected boolean indicatesPubSubConfig(BeanDefinition configDef) { - return (Boolean) configDef.getPropertyValues().getPropertyValue("pubSubDomain").getValue(); + protected boolean indicatesPubSubConfig(PropertyValues configuration) { + return (Boolean) configuration.getPropertyValue("pubSubDomain").getValue(); + } + + + protected static class ListenerContainerParserContext { + private final Element containerElement; + private final Element listenerElement; + private final PropertyValues containerValues; + private final ParserContext parserContext; + + public ListenerContainerParserContext(Element containerElement, Element listenerElement, + PropertyValues containerValues, ParserContext parserContext) { + this.containerElement = containerElement; + this.listenerElement = listenerElement; + this.containerValues = containerValues; + this.parserContext = parserContext; + } + + public Element getContainerElement() { + return containerElement; + } + + public Element getListenerElement() { + return listenerElement; + } + + public PropertyValues getContainerValues() { + return containerValues; + } + + public ParserContext getParserContext() { + return parserContext; + } + + public Object getSource() { + return parserContext.extractSource(containerElement); + } + } } diff --git a/spring-jms/src/main/java/org/springframework/jms/config/AnnotationDrivenJmsBeanDefinitionParser.java b/spring-jms/src/main/java/org/springframework/jms/config/AnnotationDrivenJmsBeanDefinitionParser.java new file mode 100644 index 0000000000..b4322abd7b --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/AnnotationDrivenJmsBeanDefinitionParser.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.AutowireCapableBeanFactory; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.util.StringUtils; + +/** + * Parser for the 'annotation-driven' element of the 'jms' namespace. + * + * @author Stephane Nicoll + * @since 4.1 + */ +final class AnnotationDrivenJmsBeanDefinitionParser implements BeanDefinitionParser { + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + Object source = parserContext.extractSource(element); + + // Register component for the surrounding element. + CompositeComponentDefinition compDefinition = new CompositeComponentDefinition(element.getTagName(), source); + parserContext.pushContainingComponent(compDefinition); + + // Nest the concrete post-processor bean in the surrounding component. + BeanDefinitionRegistry registry = parserContext.getRegistry(); + + if (registry.containsBeanDefinition(AnnotationConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)) { + parserContext.getReaderContext().error( + "Only one JmsListenerAnnotationBeanPostProcessor may exist within the context.", source); + } + else { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.jms.annotation.JmsListenerAnnotationBeanPostProcessor"); + builder.getRawBeanDefinition().setSource(source); + String endpointRegistry = element.getAttribute("registry"); + if (StringUtils.hasText(endpointRegistry)) { + builder.addPropertyReference("endpointRegistry", endpointRegistry); + } + else { + registerDefaultEndpointRegistry(source, parserContext); + } + String defaultContainerFactory = element.getAttribute("default-container-factory"); + if (StringUtils.hasText(defaultContainerFactory)) { + builder.addPropertyReference("defaultContainerFactory", defaultContainerFactory); + } + String handlerMethodFactory = element.getAttribute("handler-method-factory"); + if (StringUtils.hasText(handlerMethodFactory)) { + builder.addPropertyReference("jmsHandlerMethodFactory", handlerMethodFactory); + } + + registerInfrastructureBean(parserContext, builder, + AnnotationConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME); + } + + // Finally register the composite component. + parserContext.popAndRegisterContainingComponent(); + + return null; + } + + private static void registerDefaultEndpointRegistry(Object source, ParserContext parserContext) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition( + "org.springframework.jms.config.JmsListenerEndpointRegistry"); + builder.getRawBeanDefinition().setSource(source); + builder.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE); + registerInfrastructureBean(parserContext, builder, AnnotationConfigUtils.JMS_LISTENER_ENDPOINT_REGISTRY_BEAN_NAME); + } + + private static void registerInfrastructureBean( + ParserContext parserContext, BeanDefinitionBuilder builder, String beanName) { + + builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + parserContext.getRegistry().registerBeanDefinition(beanName, builder.getBeanDefinition()); + BeanDefinitionHolder holder = new BeanDefinitionHolder(builder.getBeanDefinition(), beanName); + parserContext.registerComponent(new BeanComponentDefinition(holder)); + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/DefaultJcaListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJcaListenerContainerFactory.java new file mode 100644 index 0000000000..ec597c1d86 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJcaListenerContainerFactory.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import javax.resource.spi.ResourceAdapter; + +import org.springframework.jms.listener.endpoint.JmsActivationSpecConfig; +import org.springframework.jms.listener.endpoint.JmsActivationSpecFactory; +import org.springframework.jms.listener.endpoint.JmsMessageEndpointManager; +import org.springframework.jms.support.destination.DestinationResolver; + +/** + * A {@link JmsListenerContainerFactory} implementation to build a + * JCA {@link JmsMessageEndpointManager}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class DefaultJcaListenerContainerFactory implements JmsListenerContainerFactory { + + private ResourceAdapter resourceAdapter; + + private JmsActivationSpecFactory activationSpecFactory; + + private DestinationResolver destinationResolver; + + private Object transactionManager; + + private JmsActivationSpecConfig activationSpecConfig; + + /** + * @see JmsMessageEndpointManager#setResourceAdapter(ResourceAdapter) + */ + public void setResourceAdapter(ResourceAdapter resourceAdapter) { + this.resourceAdapter = resourceAdapter; + } + + /** + * @see JmsMessageEndpointManager#setActivationSpecFactory(JmsActivationSpecFactory) + */ + public void setActivationSpecFactory(JmsActivationSpecFactory activationSpecFactory) { + this.activationSpecFactory = activationSpecFactory; + } + + /** + * @see JmsMessageEndpointManager#setDestinationResolver(DestinationResolver) + */ + public void setDestinationResolver(DestinationResolver destinationResolver) { + this.destinationResolver = destinationResolver; + } + + /** + * @see JmsMessageEndpointManager#setTransactionManager(Object) + */ + public void setTransactionManager(Object transactionManager) { + this.transactionManager = transactionManager; + } + + /** + * @see JmsMessageEndpointManager#setActivationSpecConfig(JmsActivationSpecConfig) + */ + public void setActivationSpecConfig(JmsActivationSpecConfig activationSpecConfig) { + this.activationSpecConfig = activationSpecConfig; + } + + /** + * Return the current {@link JmsActivationSpecConfig}. + */ + public JmsActivationSpecConfig getActivationSpecConfig() { + return activationSpecConfig; + } + + /** + * Create an empty container instance. + */ + protected JmsMessageEndpointManager createContainerInstance() { + return new JmsMessageEndpointManager(); + } + + @Override + public JmsMessageEndpointManager createMessageListenerContainer(JmsListenerEndpoint endpoint) { + if (this.destinationResolver != null && this.activationSpecFactory != null) { + throw new IllegalStateException("Specify either 'activationSpecFactory' or " + + "'destinationResolver', not both. If you define a dedicated JmsActivationSpecFactory bean, " + + "specify the custom DestinationResolver there (if possible)"); + } + + JmsMessageEndpointManager instance = createContainerInstance(); + + if (this.resourceAdapter != null) { + instance.setResourceAdapter(this.resourceAdapter); + } + if (this.activationSpecFactory != null) { + instance.setActivationSpecFactory(this.activationSpecFactory); + } + if (this.destinationResolver != null) { + instance.setDestinationResolver(this.destinationResolver); + } + if (this.transactionManager != null) { + instance.setTransactionManager(this.transactionManager); + } + if (this.activationSpecConfig != null) { + instance.setActivationSpecConfig(this.activationSpecConfig); + } + + endpoint.setupMessageContainer(instance); + + return instance; + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsHandlerMethodFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsHandlerMethodFactory.java new file mode 100644 index 0000000000..5b4f165c5f --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsHandlerMethodFactory.java @@ -0,0 +1,213 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.convert.ConversionService; +import org.springframework.format.support.DefaultFormattingConversionService; +import org.springframework.messaging.converter.GenericMessageConverter; +import org.springframework.messaging.converter.MessageConverter; +import org.springframework.messaging.handler.annotation.support.HeaderMethodArgumentResolver; +import org.springframework.messaging.handler.annotation.support.HeadersMethodArgumentResolver; +import org.springframework.messaging.handler.annotation.support.MessageMethodArgumentResolver; +import org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +/** + * The default {@link JmsHandlerMethodFactory} implementation creating an + * {@link InvocableHandlerMethod} with the necessary + * {@link HandlerMethodArgumentResolver} instances to detect and process + * all the use cases defined by {@link org.springframework.jms.annotation.JmsListener + * JmsListener}. + * + *

Extra method argument resolvers can be added to customize the method + * signature that can be handled. + * + *

By default, the validation process redirects to a no-op implementation, see + * {@link #setValidator(Validator)} to customize it. The {@link ConversionService} + * can be customized in a similar manner to tune how the message payload + * can be converted + * + * @author Stephane Nicoll + * @since 4.1 + * @see #setCustomArgumentResolvers(java.util.List) + * @see #setValidator(Validator) + * @see #setConversionService(ConversionService) + */ +public class DefaultJmsHandlerMethodFactory + implements JmsHandlerMethodFactory, InitializingBean, ApplicationContextAware { + + private ApplicationContext applicationContext; + + private ConversionService conversionService = new DefaultFormattingConversionService(); + + private MessageConverter messageConverter; + + private Validator validator = new NoOpValidator(); + + private List customArgumentResolvers + = new ArrayList(); + + private HandlerMethodArgumentResolverComposite argumentResolvers + = new HandlerMethodArgumentResolverComposite(); + + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + /** + * Set the {@link ConversionService} to use to convert the original + * message payload or headers. + * + * @see HeaderMethodArgumentResolver + * @see GenericMessageConverter + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + protected ConversionService getConversionService() { + return conversionService; + } + + /** + * Set the {@link MessageConverter} to use. By default a {@link GenericMessageConverter} + * is used. + * + * @see GenericMessageConverter + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + protected MessageConverter getMessageConverter() { + return messageConverter; + } + + /** + * Set the Validator instance used for validating @Payload arguments + * @see org.springframework.validation.annotation.Validated + * @see org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver + */ + public void setValidator(Validator validator) { + this.validator = validator; + } + + /** + * The configured Validator instance + */ + public Validator getValidator() { + return validator; + } + + /** + * Set the list of custom {@code HandlerMethodArgumentResolver}s that will be used + * after resolvers for supported argument type. + * @param customArgumentResolvers the list of resolvers; never {@code null}. + */ + public void setCustomArgumentResolvers(List customArgumentResolvers) { + Assert.notNull(customArgumentResolvers, "The 'customArgumentResolvers' cannot be null."); + this.customArgumentResolvers = customArgumentResolvers; + } + + public List getCustomArgumentResolvers() { + return customArgumentResolvers; + } + + /** + * Configure the complete list of supported argument types effectively overriding + * the ones configured by default. This is an advanced option. For most use cases + * it should be sufficient to use {@link #setCustomArgumentResolvers(java.util.List)}. + */ + public void setArgumentResolvers(List argumentResolvers) { + if (argumentResolvers == null) { + this.argumentResolvers.clear(); + return; + } + this.argumentResolvers.addResolvers(argumentResolvers); + } + + public List getArgumentResolvers() { + return this.argumentResolvers.getResolvers(); + } + + @Override + public void afterPropertiesSet() { + if (messageConverter == null) { + messageConverter = new GenericMessageConverter(getConversionService()); + } + if (this.argumentResolvers.getResolvers().isEmpty()) { + this.argumentResolvers.addResolvers(initArgumentResolvers()); + } + } + + @Override + public InvocableHandlerMethod createInvocableHandlerMethod(Object bean, Method method) { + InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(bean, method); + handlerMethod.setMessageMethodArgumentResolvers(argumentResolvers); + return handlerMethod; + } + + protected List initArgumentResolvers() { + ConfigurableBeanFactory beanFactory = + (ClassUtils.isAssignableValue(ConfigurableApplicationContext.class, applicationContext)) ? + ((ConfigurableApplicationContext) applicationContext).getBeanFactory() : null; + + List resolvers = new ArrayList(); + + // Annotation-based argument resolution + resolvers.add(new HeaderMethodArgumentResolver(getConversionService(), beanFactory)); + resolvers.add(new HeadersMethodArgumentResolver()); + + // Type-based argument resolution + resolvers.add(new MessageMethodArgumentResolver()); + + resolvers.addAll(getCustomArgumentResolvers()); + resolvers.add(new PayloadArgumentResolver(getMessageConverter(), getValidator())); + + return resolvers; + } + + + private static final class NoOpValidator implements Validator { + @Override + public boolean supports(Class clazz) { + return false; + } + + @Override + public void validate(Object target, Errors errors) { + } + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java new file mode 100644 index 0000000000..d27af25992 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java @@ -0,0 +1,145 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import java.util.concurrent.Executor; + +import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * A {@link JmsListenerContainerFactory} implementation to build regular + * {@link DefaultMessageListenerContainer}. + * + *

This should be the default for most users and a good transition + * paths for those that are used to build such container definition + * manually. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class DefaultJmsListenerContainerFactory + extends AbstractJmsListenerContainerFactory { + + private Executor taskExecutor; + + private PlatformTransactionManager transactionManager; + + private Integer cacheLevel; + + private String cacheLevelName; + + private String concurrency; + + private Integer maxMessagesPerTask; + + private Long receiveTimeout; + + private Long recoveryInterval; + + /** + * @see DefaultMessageListenerContainer#setTaskExecutor(java.util.concurrent.Executor) + */ + public void setTaskExecutor(Executor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + /** + * @see DefaultMessageListenerContainer#setTransactionManager(PlatformTransactionManager) + */ + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + /** + * @see DefaultMessageListenerContainer#setCacheLevel(int) + */ + public void setCacheLevel(Integer cacheLevel) { + this.cacheLevel = cacheLevel; + } + + /** + * @see DefaultMessageListenerContainer#setCacheLevelName(String) + */ + public void setCacheLevelName(String cacheLevelName) { + this.cacheLevelName = cacheLevelName; + } + + /** + * @see DefaultMessageListenerContainer#setConcurrency(String) + */ + public void setConcurrency(String concurrency) { + this.concurrency = concurrency; + } + + /** + * @see DefaultMessageListenerContainer#setMaxMessagesPerTask(int) + */ + public void setMaxMessagesPerTask(Integer maxMessagesPerTask) { + this.maxMessagesPerTask = maxMessagesPerTask; + } + + /** + * @see DefaultMessageListenerContainer#setReceiveTimeout(long) + */ + public void setReceiveTimeout(Long receiveTimeout) { + this.receiveTimeout = receiveTimeout; + } + + /** + * @see DefaultMessageListenerContainer#setRecoveryInterval(long) + */ + public void setRecoveryInterval(Long recoveryInterval) { + this.recoveryInterval = recoveryInterval; + } + + @Override + protected DefaultMessageListenerContainer createContainerInstance() { + return new DefaultMessageListenerContainer(); + } + + @Override + protected void initializeContainer(DefaultMessageListenerContainer container) { + if (this.taskExecutor != null) { + container.setTaskExecutor(this.taskExecutor); + } + if (this.transactionManager != null) { + container.setTransactionManager(this.transactionManager); + } + + if (this.cacheLevel != null) { + container.setCacheLevel(this.cacheLevel); + } + else if (this.cacheLevelName != null) { + container.setCacheLevelName(this.cacheLevelName); + } + + if (this.concurrency != null) { + container.setConcurrency(this.concurrency); + } + if (this.maxMessagesPerTask != null) { + container.setMaxMessagesPerTask(this.maxMessagesPerTask); + } + if (this.receiveTimeout != null) { + container.setReceiveTimeout(this.receiveTimeout); + } + if (this.recoveryInterval != null) { + container.setRecoveryInterval(this.recoveryInterval); + } + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JcaListenerContainerParser.java b/spring-jms/src/main/java/org/springframework/jms/config/JcaListenerContainerParser.java index 327c15b3e9..8bfff85455 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/JcaListenerContainerParser.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/JcaListenerContainerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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. @@ -18,6 +18,9 @@ package org.springframework.jms.config; import org.w3c.dom.Element; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -28,6 +31,7 @@ import org.springframework.util.StringUtils; * Parser for the JMS {@code <jca-listener-container>} element. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.5 */ class JcaListenerContainerParser extends AbstractListenerContainerParser { @@ -38,11 +42,68 @@ class JcaListenerContainerParser extends AbstractListenerContainerParser { @Override - protected BeanDefinition parseContainer(Element listenerEle, Element containerEle, ParserContext parserContext) { + protected PropertyValues parseProperties(Element containerEle, ParserContext parserContext) { + final MutablePropertyValues properties = new MutablePropertyValues(); + PropertyValues containerValues = parseContainerProperties(containerEle, parserContext); + + // Common values are added to the activationSpecConfig + PropertyValues commonValues = parseCommonContainerProperties(containerEle, parserContext); + BeanDefinition beanDefinition = getActivationSpecConfigBeanDefinition(containerValues); + beanDefinition.getPropertyValues().addPropertyValues(commonValues); + + properties.addPropertyValues(containerValues); + return properties; + } + + @Override + protected RootBeanDefinition createContainerFactory(String factoryId, + Element containerElement, PropertyValues propertyValues) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + beanDefinition.setBeanClassName("org.springframework.jms.config.DefaultJcaListenerContainerFactory"); + beanDefinition.getPropertyValues().addPropertyValues(propertyValues); + return beanDefinition; + } + + @Override + protected BeanDefinition createContainer(ListenerContainerParserContext context) { RootBeanDefinition containerDef = new RootBeanDefinition(); - containerDef.setSource(parserContext.extractSource(containerEle)); + containerDef.setSource(context.getSource()); containerDef.setBeanClassName("org.springframework.jms.listener.endpoint.JmsMessageEndpointManager"); + containerDef.getPropertyValues().addPropertyValues(context.getContainerValues()); + + + BeanDefinition activationSpec = getActivationSpecConfigBeanDefinition(context.getContainerValues()); + parseListenerConfiguration(context.getListenerElement(), context.getParserContext(), activationSpec); + + String phase = context.getContainerElement().getAttribute(PHASE_ATTRIBUTE); + if (StringUtils.hasText(phase)) { + containerDef.getPropertyValues().add("phase", phase); + } + + return containerDef; + } + + @Override + protected boolean indicatesPubSub(PropertyValues propertyValues) { + BeanDefinition configDef = getActivationSpecConfigBeanDefinition(propertyValues); + return indicatesPubSubConfig(configDef.getPropertyValues()); + } + + @Override + protected PropertyValue getMessageConverter(PropertyValues containerValues) { + BeanDefinition configDef = getActivationSpecConfigBeanDefinition(containerValues); + return super.getMessageConverter(configDef.getPropertyValues()); + } + + private BeanDefinition getActivationSpecConfigBeanDefinition(PropertyValues containerValues) { + PropertyValue activationSpecConfig = containerValues.getPropertyValue("activationSpecConfig"); + return (BeanDefinition) activationSpecConfig.getValue(); + } + + private PropertyValues parseContainerProperties(Element containerEle, + ParserContext parserContext) { + MutablePropertyValues propertyValues = new MutablePropertyValues(); if (containerEle.hasAttribute(RESOURCE_ADAPTER_ATTRIBUTE)) { String resourceAdapterBeanName = containerEle.getAttribute(RESOURCE_ADAPTER_ATTRIBUTE); if (!StringUtils.hasText(resourceAdapterBeanName)) { @@ -50,7 +111,7 @@ class JcaListenerContainerParser extends AbstractListenerContainerParser { "Listener container 'resource-adapter' attribute contains empty value.", containerEle); } else { - containerDef.getPropertyValues().add("resourceAdapter", + propertyValues.add("resourceAdapter", new RuntimeBeanReference(resourceAdapterBeanName)); } } @@ -63,32 +124,29 @@ class JcaListenerContainerParser extends AbstractListenerContainerParser { "'destination-resolver', not both. If you define a dedicated JmsActivationSpecFactory bean, " + "specify the custom DestinationResolver there (if possible).", containerEle); } - containerDef.getPropertyValues().add("activationSpecFactory", + propertyValues.add("activationSpecFactory", new RuntimeBeanReference(activationSpecFactoryBeanName)); } if (StringUtils.hasText(destinationResolverBeanName)) { - containerDef.getPropertyValues().add("destinationResolver", + propertyValues.add("destinationResolver", new RuntimeBeanReference(destinationResolverBeanName)); } + String transactionManagerBeanName = containerEle.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE); + if (StringUtils.hasText(transactionManagerBeanName)) { + propertyValues.add("transactionManager", + new RuntimeBeanReference(transactionManagerBeanName)); + } + RootBeanDefinition configDef = new RootBeanDefinition(); configDef.setSource(parserContext.extractSource(configDef)); configDef.setBeanClassName("org.springframework.jms.listener.endpoint.JmsActivationSpecConfig"); - parseListenerConfiguration(listenerEle, parserContext, configDef); - parseContainerConfiguration(containerEle, parserContext, configDef); Integer acknowledgeMode = parseAcknowledgeMode(containerEle, parserContext); if (acknowledgeMode != null) { configDef.getPropertyValues().add("acknowledgeMode", acknowledgeMode); } - - String transactionManagerBeanName = containerEle.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE); - if (StringUtils.hasText(transactionManagerBeanName)) { - containerDef.getPropertyValues().add("transactionManager", - new RuntimeBeanReference(transactionManagerBeanName)); - } - String concurrency = containerEle.getAttribute(CONCURRENCY_ATTRIBUTE); if (StringUtils.hasText(concurrency)) { configDef.getPropertyValues().add("concurrency", concurrency); @@ -99,21 +157,21 @@ class JcaListenerContainerParser extends AbstractListenerContainerParser { configDef.getPropertyValues().add("prefetchSize", new Integer(prefetch)); } - String phase = containerEle.getAttribute(PHASE_ATTRIBUTE); - if (StringUtils.hasText(phase)) { - containerDef.getPropertyValues().add("phase", phase); + if (containerEle.hasAttribute(MESSAGE_CONVERTER_ATTRIBUTE)) { + String messageConverter = containerEle.getAttribute(MESSAGE_CONVERTER_ATTRIBUTE); + if (!StringUtils.hasText(messageConverter)) { + parserContext.getReaderContext().error( + "listener container 'message-converter' attribute contains empty value.", containerEle); + } + else { + configDef.getPropertyValues().add("messageConverter", + new RuntimeBeanReference(messageConverter)); + } } - containerDef.getPropertyValues().add("activationSpecConfig", configDef); + propertyValues.add("activationSpecConfig", configDef); - return containerDef; - } - - @Override - protected boolean indicatesPubSub(BeanDefinition containerDef) { - BeanDefinition configDef = - (BeanDefinition) containerDef.getPropertyValues().getPropertyValue("activationSpecConfig").getValue(); - return indicatesPubSubConfig(configDef); + return propertyValues; } } diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsHandlerMethodFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsHandlerMethodFactory.java new file mode 100644 index 0000000000..a53e147467 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsHandlerMethodFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import java.lang.reflect.Method; + +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; + +/** + * A factory for {@link InvocableHandlerMethod} that is suitable to process + * an incoming JMS message. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface JmsHandlerMethodFactory { + + /** + * Create the {@link InvocableHandlerMethod} that is able to process the specified + * JMS method endpoint. + * @param bean the bean instance + * @param method the method to invoke + * @return an JMS-specific {@link InvocableHandlerMethod} suitable for that method + */ + InvocableHandlerMethod createInvocableHandlerMethod(Object bean, Method method); + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerFactory.java new file mode 100644 index 0000000000..ed21f17f9a --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import org.springframework.jms.listener.MessageListenerContainer; + +/** + * Factory of {@link MessageListenerContainer} based on a + * {@link JmsListenerEndpoint} definition. + * + * @author Stephane Nicoll + * @since 4.1 + * @see JmsListenerEndpoint + */ +public interface JmsListenerContainerFactory { + + /** + * Create a {@link MessageListenerContainer} for the given {@link JmsListenerEndpoint}. + * @param endpoint the endpoint to configure + * @return the created container + */ + C createMessageListenerContainer(JmsListenerEndpoint endpoint); + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java index f4439b05fc..0340bcfe7c 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -20,6 +20,8 @@ import javax.jms.Session; import org.w3c.dom.Element; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -31,6 +33,7 @@ import org.springframework.util.StringUtils; * * @author Mark Fisher * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.5 */ class JmsListenerContainerParser extends AbstractListenerContainerParser { @@ -52,14 +55,44 @@ class JmsListenerContainerParser extends AbstractListenerContainerParser { private static final String RECOVERY_INTERVAL_ATTRIBUTE = "recovery-interval"; + protected PropertyValues parseProperties(Element containerEle, ParserContext parserContext) { + final MutablePropertyValues properties = new MutablePropertyValues(); + PropertyValues commonValues = parseCommonContainerProperties(containerEle, parserContext); + PropertyValues containerValues = parseContainerProperties(containerEle, + parserContext, isSimpleContainer(containerEle)); + properties.addPropertyValues(commonValues); + properties.addPropertyValues(containerValues); + return properties; + } + @Override - protected BeanDefinition parseContainer(Element listenerEle, Element containerEle, ParserContext parserContext) { + protected RootBeanDefinition createContainerFactory(String factoryId, Element containerEle, PropertyValues propertyValues) { + RootBeanDefinition beanDefinition = new RootBeanDefinition(); + String containerType = containerEle.getAttribute(CONTAINER_TYPE_ATTRIBUTE); + String containerClass = containerEle.getAttribute(CONTAINER_CLASS_ATTRIBUTE); + if (!"".equals(containerClass)) { + return null; // Not supported + } + else if ("".equals(containerType) || containerType.startsWith("default")) { + beanDefinition.setBeanClassName("org.springframework.jms.config.DefaultJmsListenerContainerFactory"); + } + else if (containerType.startsWith("simple")) { + beanDefinition.setBeanClassName("org.springframework.jms.config.SimpleJmsListenerContainerFactory"); + } + beanDefinition.getPropertyValues().addPropertyValues(propertyValues); + return beanDefinition; + } + + @Override + protected BeanDefinition createContainer(ListenerContainerParserContext context) { RootBeanDefinition containerDef = new RootBeanDefinition(); - containerDef.setSource(parserContext.extractSource(containerEle)); + containerDef.setSource(context.getSource()); - parseListenerConfiguration(listenerEle, parserContext, containerDef); - parseContainerConfiguration(containerEle, parserContext, containerDef); + // Set all container values + containerDef.getPropertyValues().addPropertyValues(context.getContainerValues()); + parseListenerConfiguration(context.getListenerElement(), context.getParserContext(), containerDef); + Element containerEle = context.getContainerElement(); String containerType = containerEle.getAttribute(CONTAINER_TYPE_ATTRIBUTE); String containerClass = containerEle.getAttribute(CONTAINER_CLASS_ATTRIBUTE); if (!"".equals(containerClass)) { @@ -72,103 +105,10 @@ class JmsListenerContainerParser extends AbstractListenerContainerParser { containerDef.setBeanClassName("org.springframework.jms.listener.SimpleMessageListenerContainer"); } else { - parserContext.getReaderContext().error( + context.getParserContext().getReaderContext().error( "Invalid 'container-type' attribute: only \"default\" and \"simple\" supported.", containerEle); } - String connectionFactoryBeanName = "connectionFactory"; - if (containerEle.hasAttribute(CONNECTION_FACTORY_ATTRIBUTE)) { - connectionFactoryBeanName = containerEle.getAttribute(CONNECTION_FACTORY_ATTRIBUTE); - if (!StringUtils.hasText(connectionFactoryBeanName)) { - parserContext.getReaderContext().error( - "Listener container 'connection-factory' attribute contains empty value.", containerEle); - } - } - if (StringUtils.hasText(connectionFactoryBeanName)) { - containerDef.getPropertyValues().add("connectionFactory", - new RuntimeBeanReference(connectionFactoryBeanName)); - } - - String taskExecutorBeanName = containerEle.getAttribute(TASK_EXECUTOR_ATTRIBUTE); - if (StringUtils.hasText(taskExecutorBeanName)) { - containerDef.getPropertyValues().add("taskExecutor", - new RuntimeBeanReference(taskExecutorBeanName)); - } - - String errorHandlerBeanName = containerEle.getAttribute(ERROR_HANDLER_ATTRIBUTE); - if (StringUtils.hasText(errorHandlerBeanName)) { - containerDef.getPropertyValues().add("errorHandler", - new RuntimeBeanReference(errorHandlerBeanName)); - } - - String destinationResolverBeanName = containerEle.getAttribute(DESTINATION_RESOLVER_ATTRIBUTE); - if (StringUtils.hasText(destinationResolverBeanName)) { - containerDef.getPropertyValues().add("destinationResolver", - new RuntimeBeanReference(destinationResolverBeanName)); - } - - String cache = containerEle.getAttribute(CACHE_ATTRIBUTE); - if (StringUtils.hasText(cache)) { - if (containerType.startsWith("simple")) { - if (!("auto".equals(cache) || "consumer".equals(cache))) { - parserContext.getReaderContext().warning( - "'cache' attribute not actively supported for listener container of type \"simple\". " + - "Effective runtime behavior will be equivalent to \"consumer\" / \"auto\".", containerEle); - } - } - else { - containerDef.getPropertyValues().add("cacheLevelName", "CACHE_" + cache.toUpperCase()); - } - } - - Integer acknowledgeMode = parseAcknowledgeMode(containerEle, parserContext); - if (acknowledgeMode != null) { - if (acknowledgeMode == Session.SESSION_TRANSACTED) { - containerDef.getPropertyValues().add("sessionTransacted", Boolean.TRUE); - } - else { - containerDef.getPropertyValues().add("sessionAcknowledgeMode", acknowledgeMode); - } - } - - String transactionManagerBeanName = containerEle.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE); - if (StringUtils.hasText(transactionManagerBeanName)) { - if (containerType.startsWith("simple")) { - parserContext.getReaderContext().error( - "'transaction-manager' attribute not supported for listener container of type \"simple\".", containerEle); - } - else { - containerDef.getPropertyValues().add("transactionManager", - new RuntimeBeanReference(transactionManagerBeanName)); - } - } - - String concurrency = containerEle.getAttribute(CONCURRENCY_ATTRIBUTE); - if (StringUtils.hasText(concurrency)) { - containerDef.getPropertyValues().add("concurrency", concurrency); - } - - String prefetch = containerEle.getAttribute(PREFETCH_ATTRIBUTE); - if (StringUtils.hasText(prefetch)) { - if (containerType.startsWith("default")) { - containerDef.getPropertyValues().add("maxMessagesPerTask", prefetch); - } - } - - String receiveTimeout = containerEle.getAttribute(RECEIVE_TIMEOUT_ATTRIBUTE); - if (StringUtils.hasText(receiveTimeout)) { - if (containerType.startsWith("default")) { - containerDef.getPropertyValues().add("receiveTimeout", receiveTimeout); - } - } - - String recoveryInterval = containerEle.getAttribute(RECOVERY_INTERVAL_ATTRIBUTE); - if (StringUtils.hasText(recoveryInterval)) { - if (containerType.startsWith("default")) { - containerDef.getPropertyValues().add("recoveryInterval", recoveryInterval); - } - } - String phase = containerEle.getAttribute(PHASE_ATTRIBUTE); if (StringUtils.hasText(phase)) { containerDef.getPropertyValues().add("phase", phase); @@ -178,8 +118,124 @@ class JmsListenerContainerParser extends AbstractListenerContainerParser { } @Override - protected boolean indicatesPubSub(BeanDefinition containerDef) { - return indicatesPubSubConfig(containerDef); + protected boolean indicatesPubSub(PropertyValues propertyValues) { + return indicatesPubSubConfig(propertyValues); + } + + private PropertyValues parseContainerProperties(Element containerEle, + ParserContext parserContext, boolean isSimpleContainer) { + MutablePropertyValues propertyValues = new MutablePropertyValues(); + String connectionFactoryBeanName = "connectionFactory"; + if (containerEle.hasAttribute(CONNECTION_FACTORY_ATTRIBUTE)) { + connectionFactoryBeanName = containerEle.getAttribute(CONNECTION_FACTORY_ATTRIBUTE); + if (!StringUtils.hasText(connectionFactoryBeanName)) { + parserContext.getReaderContext().error( + "Listener container 'connection-factory' attribute contains empty value.", containerEle); + } + } + if (StringUtils.hasText(connectionFactoryBeanName)) { + propertyValues.add("connectionFactory", + new RuntimeBeanReference(connectionFactoryBeanName)); + } + + String taskExecutorBeanName = containerEle.getAttribute(TASK_EXECUTOR_ATTRIBUTE); + if (StringUtils.hasText(taskExecutorBeanName)) { + propertyValues.add("taskExecutor", + new RuntimeBeanReference(taskExecutorBeanName)); + } + + String errorHandlerBeanName = containerEle.getAttribute(ERROR_HANDLER_ATTRIBUTE); + if (StringUtils.hasText(errorHandlerBeanName)) { + propertyValues.add("errorHandler", + new RuntimeBeanReference(errorHandlerBeanName)); + } + + if (containerEle.hasAttribute(MESSAGE_CONVERTER_ATTRIBUTE)) { + String messageConverter = containerEle.getAttribute(MESSAGE_CONVERTER_ATTRIBUTE); + if (!StringUtils.hasText(messageConverter)) { + parserContext.getReaderContext().error( + "listener container 'message-converter' attribute contains empty value.", containerEle); + } + else { + propertyValues.add("messageConverter", + new RuntimeBeanReference(messageConverter)); + } + } + + String destinationResolverBeanName = containerEle.getAttribute(DESTINATION_RESOLVER_ATTRIBUTE); + if (StringUtils.hasText(destinationResolverBeanName)) { + propertyValues.add("destinationResolver", + new RuntimeBeanReference(destinationResolverBeanName)); + } + + String cache = containerEle.getAttribute(CACHE_ATTRIBUTE); + if (StringUtils.hasText(cache)) { + if (isSimpleContainer) { + if (!("auto".equals(cache) || "consumer".equals(cache))) { + parserContext.getReaderContext().warning( + "'cache' attribute not actively supported for listener container of type \"simple\". " + + "Effective runtime behavior will be equivalent to \"consumer\" / \"auto\".", containerEle); + } + } + else { + propertyValues.add("cacheLevelName", "CACHE_" + cache.toUpperCase()); + } + } + + Integer acknowledgeMode = parseAcknowledgeMode(containerEle, parserContext); + if (acknowledgeMode != null) { + if (acknowledgeMode == Session.SESSION_TRANSACTED) { + propertyValues.add("sessionTransacted", Boolean.TRUE); + } + else { + propertyValues.add("sessionAcknowledgeMode", acknowledgeMode); + } + } + + String transactionManagerBeanName = containerEle.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE); + if (StringUtils.hasText(transactionManagerBeanName)) { + if (isSimpleContainer) { + parserContext.getReaderContext().error( + "'transaction-manager' attribute not supported for listener container of type \"simple\".", containerEle); + } + else { + propertyValues.add("transactionManager", + new RuntimeBeanReference(transactionManagerBeanName)); + } + } + + String concurrency = containerEle.getAttribute(CONCURRENCY_ATTRIBUTE); + if (StringUtils.hasText(concurrency)) { + propertyValues.add("concurrency", concurrency); + } + + String prefetch = containerEle.getAttribute(PREFETCH_ATTRIBUTE); + if (StringUtils.hasText(prefetch)) { + if (!isSimpleContainer) { + propertyValues.add("maxMessagesPerTask", prefetch); + } + } + + String receiveTimeout = containerEle.getAttribute(RECEIVE_TIMEOUT_ATTRIBUTE); + if (StringUtils.hasText(receiveTimeout)) { + if (!isSimpleContainer) { + propertyValues.add("receiveTimeout", receiveTimeout); + } + } + + String recoveryInterval = containerEle.getAttribute(RECOVERY_INTERVAL_ATTRIBUTE); + if (StringUtils.hasText(recoveryInterval)) { + if (!isSimpleContainer) { + propertyValues.add("recoveryInterval", recoveryInterval); + } + } + + return propertyValues; + } + + private boolean isSimpleContainer(Element containerEle) { + String containerType = containerEle.getAttribute(CONTAINER_TYPE_ATTRIBUTE); + return containerType.startsWith("simple"); } } diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpoint.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpoint.java new file mode 100644 index 0000000000..afbd67056f --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpoint.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import org.springframework.jms.listener.MessageListenerContainer; + +/** + * Model for a JMS listener endpoint. Can be used against a + * {@link org.springframework.jms.annotation.JmsListenerConfigurer + * JmsListenerConfigurer} to register endpoints programmatically. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface JmsListenerEndpoint { + + /** + * Return the id of this endpoint. + */ + String getId(); + + /** + * Setup the specified message listener container with the model + * defined by this endpoint. + *

This endpoint must provide the requested missing option(s) of + * the specified container to make it usable. Usually, this is about + * setting the {@code destination} and the {@code messageListener} to + * use but an implementation may override any default setting that + * was already set. + * @param container the container to configure + */ + void setupMessageContainer(MessageListenerContainer container); + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java new file mode 100644 index 0000000000..e14ff41596 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * Helper bean for registering {@link JmsListenerEndpoint} with + * a {@link JmsListenerEndpointRegistry}. + * + * @author Stephane Nicoll + * @since 4.1 + * @see org.springframework.jms.annotation.JmsListenerConfigurer + */ +public class JmsListenerEndpointRegistrar implements InitializingBean { + + private JmsListenerEndpointRegistry endpointRegistry; + + private JmsListenerContainerFactory defaultContainerFactory; + + private JmsHandlerMethodFactory jmsHandlerMethodFactory; + + private final List endpointDescriptors + = new ArrayList(); + + /** + * Set the {@link JmsListenerEndpointRegistry} instance to use. + */ + public void setEndpointRegistry(JmsListenerEndpointRegistry endpointRegistry) { + this.endpointRegistry = endpointRegistry; + } + + /** + * Return the {@link JmsListenerEndpointRegistry} instance for this + * registrar, may be {@code null}. + */ + public JmsListenerEndpointRegistry getEndpointRegistry() { + return endpointRegistry; + } + + /** + * Set the default {@link JmsListenerContainerFactory} to use in case a + * {@link JmsListenerEndpoint} is registered with a {@code null} container + * factory. + */ + public void setDefaultContainerFactory(JmsListenerContainerFactory defaultContainerFactory) { + this.defaultContainerFactory = defaultContainerFactory; + } + + /** + * Return the {@link JmsListenerContainerFactory} to use if none has been + * defined for a particular endpoint or {@code null} if no default is set. + */ + public JmsListenerContainerFactory getDefaultContainerFactory() { + return defaultContainerFactory; + } + + /** + * Set the {@link JmsHandlerMethodFactory} to use to configure the message + * listener responsible to serve an endpoint detected by this processor. + *

By default, {@link DefaultJmsHandlerMethodFactory} is used and it + * can be configured further to support additional method arguments + * or to customize conversion and validation support. See + * {@link DefaultJmsHandlerMethodFactory} javadoc for more details. + */ + public void setJmsHandlerMethodFactory(JmsHandlerMethodFactory jmsHandlerMethodFactory) { + this.jmsHandlerMethodFactory = jmsHandlerMethodFactory; + } + + /** + * Return the custom {@link JmsHandlerMethodFactory} to use, if any. + */ + public JmsHandlerMethodFactory getJmsHandlerMethodFactory() { + return jmsHandlerMethodFactory; + } + + /** + * Register a new {@link JmsListenerEndpoint} alongside the {@link JmsListenerContainerFactory} + * to use to create the underlying container. + *

The {@code factory} may be {@code null} if the default factory has to be used for that + * endpoint. + */ + public void registerEndpoint(JmsListenerEndpoint endpoint, JmsListenerContainerFactory factory) { + Assert.notNull(endpoint, "Endpoint must be set"); + Assert.notNull(endpoint.getId(), "Endpoint id must be set"); + // Factory may be null, we defer the resolution right before actually creating the container + this.endpointDescriptors.add(new JmsListenerEndpointDescriptor(endpoint, factory)); + } + + /** + * Register a new {@link JmsListenerEndpoint} using the default {@link JmsListenerContainerFactory} + * to create the underlying container. + * + * @see #setDefaultContainerFactory(JmsListenerContainerFactory) + * @see #registerEndpoint(JmsListenerEndpoint, JmsListenerContainerFactory) + */ + public void registerEndpoint(JmsListenerEndpoint endpoint) { + registerEndpoint(endpoint, null); + } + + @Override + public void afterPropertiesSet() throws Exception { + startAllEndpoints(); + } + + protected void startAllEndpoints() throws Exception { + for (JmsListenerEndpointDescriptor descriptor : endpointDescriptors) { + endpointRegistry.createJmsListenerContainer( + descriptor.endpoint, resolveContainerFactory(descriptor)); + } + } + + private JmsListenerContainerFactory resolveContainerFactory(JmsListenerEndpointDescriptor descriptor) { + if (descriptor.containerFactory != null) { + return descriptor.containerFactory; + } + else if (defaultContainerFactory != null) { + return defaultContainerFactory; + } + else { + throw new IllegalStateException("Could not resolve the " + + JmsListenerContainerFactory.class.getSimpleName() + " to use for [" + + descriptor.endpoint + "] no factory was given and no default is set."); + } + } + + + private static class JmsListenerEndpointDescriptor { + private final JmsListenerEndpoint endpoint; + + private final JmsListenerContainerFactory containerFactory; + + private JmsListenerEndpointDescriptor(JmsListenerEndpoint endpoint, + JmsListenerContainerFactory containerFactory) { + this.endpoint = endpoint; + this.containerFactory = containerFactory; + } + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistry.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistry.java new file mode 100644 index 0000000000..20e6026197 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistry.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jms.listener.MessageListenerContainer; +import org.springframework.util.Assert; + +/** + * Create the necessary {@link MessageListenerContainer} instances + * for the registered {@linkplain JmsListenerEndpoint endpoints}. Also + * manage the lifecycle of the containers, in particular with the + * lifecycle of the application context. + * + *

Contrary to {@link MessageListenerContainer} created manually, + * containers managed by this instances are not registered in the + * application context and are not candidates for autowiring. Use + * {@link #getContainers()} if you need to access the containers + * of this instance for management purposes. If you need to access + * a particular container, use {@link #getContainer(String)} with the + * id of the endpoint. + * + * @author Stephane Nicoll + * @since 4.1 + * @see JmsListenerEndpoint + * @see MessageListenerContainer + * @see JmsListenerContainerFactory + */ +public class JmsListenerEndpointRegistry implements DisposableBean { + + private final Map containers = + new HashMap(); + + /** + * Return the {@link MessageListenerContainer} with the specified id or + * {@code null} if no such container exists. + * @param id the id of the container + * @return the container or {@code null} if no container with that id exists + * @see JmsListenerEndpoint#getId() + */ + public MessageListenerContainer getContainer(String id) { + Assert.notNull(id, "the container identifier must be set."); + return containers.get(id); + } + + /** + * Return the managed {@link MessageListenerContainer} instance(s). + */ + public Collection getContainers() { + return Collections.unmodifiableCollection(containers.values()); + } + + /** + * Create a message listener container for the given {@link JmsListenerEndpoint}. + *

This create the necessary infrastructure to honor that endpoint + * with regards to its configuration. + * @param endpoint the endpoint to add + * @see #getContainers() + * @see #getContainer(String) + */ + public void createJmsListenerContainer(JmsListenerEndpoint endpoint, JmsListenerContainerFactory factory) { + Assert.notNull(endpoint, "Endpoint must not be null"); + Assert.notNull(factory, "Factory must not be null"); + + String id = endpoint.getId(); + Assert.notNull(id, "Endpoint id must not be null"); + Assert.state(!containers.containsKey(id), "another endpoint is already " + + "registered with id '" + id + "'"); + + MessageListenerContainer container = doCreateJmsListenerContainer(endpoint, factory); + containers.put(id, container); + } + + /** + * Create and start a new container using the specified factory. + */ + protected MessageListenerContainer doCreateJmsListenerContainer(JmsListenerEndpoint endpoint, + JmsListenerContainerFactory factory) { + MessageListenerContainer container = factory.createMessageListenerContainer(endpoint); + initializeContainer(container); + return container; + } + + @Override + public void destroy() throws Exception { + for (MessageListenerContainer container : getContainers()) { + stopContainer(container); + } + } + + protected void initializeContainer(MessageListenerContainer container) { + container.start(); + if (container instanceof InitializingBean) { + try { + ((InitializingBean) container).afterPropertiesSet(); + } + catch (Exception e) { + throw new BeanInitializationException("Could not start message listener container", e); + } + } + } + + protected void stopContainer(MessageListenerContainer container) throws Exception { + container.stop(); + if (container instanceof DisposableBean) { + ((DisposableBean) container).destroy(); + } + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsNamespaceHandler.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsNamespaceHandler.java index 7b78bd093b..f23ef6a56b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/JmsNamespaceHandler.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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. @@ -24,6 +24,7 @@ import org.springframework.beans.factory.xml.NamespaceHandlerSupport; * * @author Mark Fisher * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.5 */ public class JmsNamespaceHandler extends NamespaceHandlerSupport { @@ -32,6 +33,7 @@ public class JmsNamespaceHandler extends NamespaceHandlerSupport { public void init() { registerBeanDefinitionParser("listener-container", new JmsListenerContainerParser()); registerBeanDefinitionParser("jca-listener-container", new JcaListenerContainerParser()); + registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenJmsBeanDefinitionParser()); } } diff --git a/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java b/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java new file mode 100644 index 0000000000..f8ff3e4173 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import java.lang.reflect.Method; + +import org.springframework.jms.listener.MessageListenerContainer; +import org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.util.Assert; + +/** + * A {@link JmsListenerEndpoint} providing the method to invoke to process + * an incoming message for this endpoint. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class MethodJmsListenerEndpoint extends AbstractJmsListenerEndpoint { + + private Object bean; + + private Method method; + + private String responseDestination; + + private JmsHandlerMethodFactory jmsHandlerMethodFactory; + + /** + * Set the object instance that should manage this endpoint. + */ + public void setBean(Object bean) { + this.bean = bean; + } + + public Object getBean() { + return bean; + } + + /** + * Set the method to invoke to process a message managed by this + * endpoint. + */ + public void setMethod(Method method) { + this.method = method; + } + + public Method getMethod() { + return method; + } + + /** + * Set the name of the default response destination to send response messages to. + */ + public void setResponseDestination(String responseDestination) { + this.responseDestination = responseDestination; + } + + /** + * Return the name of the default response destination to send response messages to. + */ + public String getResponseDestination() { + return responseDestination; + } + + /** + * Set the {@link DefaultJmsHandlerMethodFactory} to use to build the + * {@link InvocableHandlerMethod} responsible to manage the invocation + * of this endpoint. + */ + public void setJmsHandlerMethodFactory(JmsHandlerMethodFactory jmsHandlerMethodFactory) { + this.jmsHandlerMethodFactory = jmsHandlerMethodFactory; + } + + @Override + protected MessagingMessageListenerAdapter createMessageListener(MessageListenerContainer container) { + Assert.state(jmsHandlerMethodFactory != null, + "Could not create message listener, message listener factory not set."); + MessagingMessageListenerAdapter messageListener = new MessagingMessageListenerAdapter(); + InvocableHandlerMethod invocableHandlerMethod = + jmsHandlerMethodFactory.createInvocableHandlerMethod(getBean(), getMethod()); + messageListener.setHandlerMethod(invocableHandlerMethod); + String responseDestination = getResponseDestination(); + if (responseDestination != null) { + if (isQueue()) { + messageListener.setDefaultResponseQueueName(responseDestination); + } + else { + messageListener.setDefaultResponseTopicName(responseDestination); + } + } + MessageConverter messageConverter = container.getMessageConverter(); + if (messageConverter != null) { + messageListener.setMessageConverter(messageConverter); + } + return messageListener; + } + + @Override + protected StringBuilder getEndpointDescription() { + return super.getEndpointDescription() + .append(" | bean='") + .append(this.bean) + .append("'") + .append(" | method='") + .append(this.method) + .append("'"); + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerContainerFactory.java new file mode 100644 index 0000000000..32bd777006 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerContainerFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import org.springframework.jms.listener.SimpleMessageListenerContainer; + +/** + * A {@link JmsListenerContainerFactory} implementation to build + * a {@link SimpleMessageListenerContainer}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class SimpleJmsListenerContainerFactory + extends AbstractJmsListenerContainerFactory { + + @Override + protected SimpleMessageListenerContainer createContainerInstance() { + return new SimpleMessageListenerContainer(); + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerEndpoint.java b/spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerEndpoint.java new file mode 100644 index 0000000000..2bc184065e --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerEndpoint.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import javax.jms.MessageListener; + +import org.springframework.jms.listener.MessageListenerContainer; + +/** + * A {@link JmsListenerEndpoint} simply providing the {@link MessageListener} to + * invoke to process an incoming message for this endpoint. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class SimpleJmsListenerEndpoint extends AbstractJmsListenerEndpoint { + + private MessageListener messageListener; + + /** + * Return the {@link MessageListener} to invoke when a message matching + * the endpoint is received. + */ + public MessageListener getMessageListener() { + return messageListener; + } + + public void setMessageListener(MessageListener messageListener) { + this.messageListener = messageListener; + } + + @Override + protected MessageListener createMessageListener(MessageListenerContainer container) { + return getMessageListener(); + } + + @Override + protected StringBuilder getEndpointDescription() { + return super.getEndpointDescription() + .append(" | messageListener='") + .append(this.messageListener) + .append("'"); + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/config/package-info.java b/spring-jms/src/main/java/org/springframework/jms/config/package-info.java index 0c7ba80b05..4a8023ab98 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/package-info.java @@ -1,9 +1,6 @@ - /** - * * Support package for declarative messaging configuration, - * with XML schema being the primary configuration format. - * + * with Java configuration and XML schema support. */ package org.springframework.jms.config; diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java index 120660a985..2d6322a26f 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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. @@ -27,6 +27,7 @@ import javax.jms.Session; import javax.jms.Topic; import org.springframework.jms.support.JmsUtils; +import org.springframework.jms.support.converter.MessageConverter; import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; @@ -114,6 +115,7 @@ import org.springframework.util.ErrorHandler; * not just direct JMS Session usage in a {@link SessionAwareMessageListener}. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.0 * @see #setMessageListener * @see javax.jms.MessageListener @@ -123,7 +125,8 @@ import org.springframework.util.ErrorHandler; * @see SimpleMessageListenerContainer * @see org.springframework.jms.listener.endpoint.JmsMessageEndpointManager */ -public abstract class AbstractMessageListenerContainer extends AbstractJmsListeningContainer { +public abstract class AbstractMessageListenerContainer + extends AbstractJmsListeningContainer implements MessageListenerContainer { private volatile Object destination; @@ -139,6 +142,8 @@ public abstract class AbstractMessageListenerContainer extends AbstractJmsListen private ErrorHandler errorHandler; + private MessageConverter messageConverter; + private boolean exposeListenerSession = true; private boolean acceptMessagesWhileStopping = false; @@ -358,6 +363,19 @@ public abstract class AbstractMessageListenerContainer extends AbstractJmsListen this.errorHandler = errorHandler; } + /** + * Set the {@link MessageConverter} strategy for converting JMS Messages. + * @param messageConverter the message converter to use + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + @Override + public MessageConverter getMessageConverter() { + return messageConverter; + } + /** * Set whether to expose the listener JMS Session to a registered * {@link SessionAwareMessageListener} as well as to @@ -420,6 +438,10 @@ public abstract class AbstractMessageListenerContainer extends AbstractJmsListen } } + @Override + public void setupMessageListener(Object messageListener) { + setMessageListener(messageListener); + } //------------------------------------------------------------------------- // Template methods for listener execution diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/MessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/MessageListenerContainer.java new file mode 100644 index 0000000000..bdae48d121 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/listener/MessageListenerContainer.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.listener; + +import org.springframework.context.Lifecycle; +import org.springframework.jms.support.converter.MessageConverter; + +/** + * Internal abstraction used by the framework representing a message + * listener container. Not meant to be implemented externally with + * support for both JMS and JCA style containers. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface MessageListenerContainer extends Lifecycle { + + /** + * Setup the message listener to use. Throws an {@link IllegalArgumentException} + * if that message listener type is not supported. + */ + void setupMessageListener(Object messageListener); + + /** + * Return the {@link MessageConverter} that can be used to + * convert {@link javax.jms.Message}, if any. + */ + MessageConverter getMessageConverter(); + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java new file mode 100644 index 0000000000..82faef5c3c --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java @@ -0,0 +1,413 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.listener.adapter; + +import javax.jms.Destination; +import javax.jms.InvalidDestinationException; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.jms.MessageProducer; +import javax.jms.Session; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jms.listener.SessionAwareMessageListener; +import org.springframework.jms.support.JmsUtils; +import org.springframework.jms.support.converter.JmsHeaderMapper; +import org.springframework.jms.support.converter.MessageConversionException; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.jms.support.converter.SimpleJmsHeaderMapper; +import org.springframework.jms.support.converter.SimpleMessageConverter; +import org.springframework.jms.support.destination.DestinationResolver; +import org.springframework.jms.support.destination.DynamicDestinationResolver; +import org.springframework.util.Assert; + +/** + * An abstract {@link MessageListener} adapter providing the necessary infrastructure + * to extract the payload of a {@link Message} + * + * @author Juergen Hoeller + * @author Stephane Nicoll + * @since 4.1 + * @see MessageListener + * @see SessionAwareMessageListener + */ +public abstract class AbstractAdaptableMessageListener + implements MessageListener, SessionAwareMessageListener { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private Object defaultResponseDestination; + + private DestinationResolver destinationResolver = new DynamicDestinationResolver(); + + private MessageConverter messageConverter; + + private JmsHeaderMapper headerMapper = new SimpleJmsHeaderMapper(); + + + /** + * Create a new instance with default settings. + */ + protected AbstractAdaptableMessageListener() { + initDefaultStrategies(); + } + + /** + * Set the default destination to send response messages to. This will be applied + * in case of a request message that does not carry a "JMSReplyTo" field. + *

Response destinations are only relevant for listener methods that return + * result objects, which will be wrapped in a response message and sent to a + * response destination. + *

Alternatively, specify a "defaultResponseQueueName" or "defaultResponseTopicName", + * to be dynamically resolved via the DestinationResolver. + * @see #setDefaultResponseQueueName(String) + * @see #setDefaultResponseTopicName(String) + * @see #getResponseDestination + */ + public void setDefaultResponseDestination(Destination destination) { + this.defaultResponseDestination = destination; + } + + /** + * Set the name of the default response queue to send response messages to. + * This will be applied in case of a request message that does not carry a + * "JMSReplyTo" field. + *

Alternatively, specify a JMS Destination object as "defaultResponseDestination". + * @see #setDestinationResolver + * @see #setDefaultResponseDestination(javax.jms.Destination) + */ + public void setDefaultResponseQueueName(String destinationName) { + this.defaultResponseDestination = new DestinationNameHolder(destinationName, false); + } + + /** + * Set the name of the default response topic to send response messages to. + * This will be applied in case of a request message that does not carry a + * "JMSReplyTo" field. + *

Alternatively, specify a JMS Destination object as "defaultResponseDestination". + * @see #setDestinationResolver + * @see #setDefaultResponseDestination(javax.jms.Destination) + */ + public void setDefaultResponseTopicName(String destinationName) { + this.defaultResponseDestination = new DestinationNameHolder(destinationName, true); + } + + /** + * Set the DestinationResolver that should be used to resolve response + * destination names for this adapter. + *

The default resolver is a DynamicDestinationResolver. Specify a + * JndiDestinationResolver for resolving destination names as JNDI locations. + * @see org.springframework.jms.support.destination.DynamicDestinationResolver + * @see org.springframework.jms.support.destination.JndiDestinationResolver + */ + public void setDestinationResolver(DestinationResolver destinationResolver) { + Assert.notNull(destinationResolver, "DestinationResolver must not be null"); + this.destinationResolver = destinationResolver; + } + + /** + * Return the DestinationResolver for this adapter. + */ + protected DestinationResolver getDestinationResolver() { + return this.destinationResolver; + } + + /** + * Set the converter that will convert incoming JMS messages to + * listener method arguments, and objects returned from listener + * methods back to JMS messages. + *

The default converter is a {@link SimpleMessageConverter}, which is able + * to handle {@link javax.jms.BytesMessage BytesMessages}, + * {@link javax.jms.TextMessage TextMessages} and + * {@link javax.jms.ObjectMessage ObjectMessages}. + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + /** + * Return the converter that will convert incoming JMS messages to + * listener method arguments, and objects returned from listener + * methods back to JMS messages. + */ + protected MessageConverter getMessageConverter() { + return this.messageConverter; + } + + /** + * Set the {@link JmsHeaderMapper} implementation to use to map the + * standard JMS headers. By default {@link SimpleJmsHeaderMapper} is + * used + * @see SimpleJmsHeaderMapper + */ + public void setHeaderMapper(JmsHeaderMapper headerMapper) { + Assert.notNull(headerMapper, "HeaderMapper must not be null"); + this.headerMapper = headerMapper; + } + + /** + * Return the {@link JmsHeaderMapper} that converts headers from + * and to the messaging abstraction. + */ + protected JmsHeaderMapper getHeaderMapper() { + return headerMapper; + } + + /** + * Standard JMS {@link MessageListener} entry point. + *

Delegates the message to the target listener method, with appropriate + * conversion of the message argument. In case of an exception, the + * {@link #handleListenerException(Throwable)} method will be invoked. + *

Note: Does not support sending response messages based on + * result objects returned from listener methods. Use the + * {@link SessionAwareMessageListener} entry point (typically through a Spring + * message listener container) for handling result objects as well. + * @param message the incoming JMS message + * @see #handleListenerException + * @see #onMessage(javax.jms.Message, javax.jms.Session) + */ + @Override + public void onMessage(Message message) { + try { + onMessage(message, null); + } + catch (Throwable ex) { + handleListenerException(ex); + } + } + + /** + * Initialize the default implementations for the adapter's strategies. + * @see #setMessageConverter + * @see org.springframework.jms.support.converter.SimpleMessageConverter + */ + protected void initDefaultStrategies() { + setMessageConverter(new SimpleMessageConverter()); + } + + /** + * Handle the given exception that arose during listener execution. + * The default implementation logs the exception at error level. + *

This method only applies when used as standard JMS {@link MessageListener}. + * In case of the Spring {@link SessionAwareMessageListener} mechanism, + * exceptions get handled by the caller instead. + * @param ex the exception to handle + * @see #onMessage(javax.jms.Message) + */ + protected void handleListenerException(Throwable ex) { + logger.error("Listener execution failed", ex); + } + + /** + * Extract the message body from the given JMS message. + * @param message the JMS {@code Message} + * @return the content of the message, to be passed into the + * listener method as argument + * @throws JMSException if thrown by JMS API methods + */ + protected Object extractMessage(Message message) throws JMSException { + MessageConverter converter = getMessageConverter(); + if (converter != null) { + return converter.fromMessage(message); + } + return message; + } + + /** + * Handle the given result object returned from the listener method, + * sending a response message back. + * @param result the result object to handle (never {@code null}) + * @param request the original request message + * @param session the JMS Session to operate on (may be {@code null}) + * @throws JMSException if thrown by JMS API methods + * @see #buildMessage + * @see #postProcessResponse + * @see #getResponseDestination + * @see #sendResponse + */ + protected void handleResult(Object result, Message request, Session session) throws JMSException { + if (session != null) { + if (logger.isDebugEnabled()) { + logger.debug("Listener method returned result [" + result + + "] - generating response message for it"); + } + Message response = buildMessage(session, result); + postProcessResponse(request, response); + Destination destination = getResponseDestination(request, response, session); + sendResponse(session, destination, response); + } + else { + if (logger.isWarnEnabled()) { + logger.warn("Listener method returned result [" + result + + "]: not generating response message for it because of no JMS Session given"); + } + } + } + + /** + * Build a JMS message to be sent as response based on the given result object. + * @param session the JMS Session to operate on + * @param result the content of the message, as returned from the listener method + * @return the JMS {@code Message} (never {@code null}) + * @throws JMSException if thrown by JMS API methods + * @see #setMessageConverter + */ + protected Message buildMessage(Session session, Object result) throws JMSException { + MessageConverter converter = getMessageConverter(); + if (converter != null) { + if (result instanceof org.springframework.messaging.Message) { + org.springframework.messaging.Message message = (org.springframework.messaging.Message) result; + Message reply = converter.toMessage(message.getPayload(), session); + getHeaderMapper().fromHeaders(message.getHeaders(), reply); + return reply; + } + else { + return converter.toMessage(result, session); + } + } + else { + if (!(result instanceof Message)) { + throw new MessageConversionException( + "No MessageConverter specified - cannot handle message [" + result + "]"); + } + return (Message) result; + } + } + + /** + * Post-process the given response message before it will be sent. + *

The default implementation sets the response's correlation id + * to the request message's correlation id, if any; otherwise to the + * request message id. + * @param request the original incoming JMS message + * @param response the outgoing JMS message about to be sent + * @throws JMSException if thrown by JMS API methods + * @see javax.jms.Message#setJMSCorrelationID + */ + protected void postProcessResponse(Message request, Message response) throws JMSException { + String correlation = request.getJMSCorrelationID(); + if (correlation == null) { + correlation = request.getJMSMessageID(); + } + response.setJMSCorrelationID(correlation); + } + + /** + * Determine a response destination for the given message. + *

The default implementation first checks the JMS Reply-To + * {@link Destination} of the supplied request; if that is not {@code null} + * it is returned; if it is {@code null}, then the configured + * {@link #resolveDefaultResponseDestination default response destination} + * is returned; if this too is {@code null}, then an + * {@link javax.jms.InvalidDestinationException} is thrown. + * @param request the original incoming JMS message + * @param response the outgoing JMS message about to be sent + * @param session the JMS Session to operate on + * @return the response destination (never {@code null}) + * @throws JMSException if thrown by JMS API methods + * @throws javax.jms.InvalidDestinationException if no {@link Destination} can be determined + * @see #setDefaultResponseDestination + * @see javax.jms.Message#getJMSReplyTo() + */ + protected Destination getResponseDestination(Message request, Message response, Session session) + throws JMSException { + + Destination replyTo = request.getJMSReplyTo(); + if (replyTo == null) { + replyTo = resolveDefaultResponseDestination(session); + if (replyTo == null) { + throw new InvalidDestinationException("Cannot determine response destination: " + + "Request message does not contain reply-to destination, and no default response destination set."); + } + } + return replyTo; + } + + /** + * Resolve the default response destination into a JMS {@link Destination}, using this + * accessor's {@link DestinationResolver} in case of a destination name. + * @return the located {@link Destination} + * @throws javax.jms.JMSException if resolution failed + * @see #setDefaultResponseDestination + * @see #setDefaultResponseQueueName + * @see #setDefaultResponseTopicName + * @see #setDestinationResolver + */ + protected Destination resolveDefaultResponseDestination(Session session) throws JMSException { + if (this.defaultResponseDestination instanceof Destination) { + return (Destination) this.defaultResponseDestination; + } + if (this.defaultResponseDestination instanceof DestinationNameHolder) { + DestinationNameHolder nameHolder = (DestinationNameHolder) this.defaultResponseDestination; + return getDestinationResolver().resolveDestinationName(session, nameHolder.name, nameHolder.isTopic); + } + return null; + } + + /** + * Send the given response message to the given destination. + * @param response the JMS message to send + * @param destination the JMS destination to send to + * @param session the JMS session to operate on + * @throws JMSException if thrown by JMS API methods + * @see #postProcessProducer + * @see javax.jms.Session#createProducer + * @see javax.jms.MessageProducer#send + */ + protected void sendResponse(Session session, Destination destination, Message response) throws JMSException { + MessageProducer producer = session.createProducer(destination); + try { + postProcessProducer(producer, response); + producer.send(response); + } + finally { + JmsUtils.closeMessageProducer(producer); + } + } + + /** + * Post-process the given message producer before using it to send the response. + *

The default implementation is empty. + * @param producer the JMS message producer that will be used to send the message + * @param response the outgoing JMS message about to be sent + * @throws JMSException if thrown by JMS API methods + */ + protected void postProcessProducer(MessageProducer producer, Message response) throws JMSException { + } + + + /** + * Internal class combining a destination name + * and its target destination type (queue or topic). + */ + private static class DestinationNameHolder { + + public final String name; + + public final boolean isTopic; + + public DestinationNameHolder(String name, boolean isTopic) { + this.name = name; + this.isTopic = isTopic; + } + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java index a35b56149b..d21974dd7b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2014 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. @@ -17,25 +17,16 @@ package org.springframework.jms.listener.adapter; import java.lang.reflect.InvocationTargetException; -import javax.jms.Destination; -import javax.jms.InvalidDestinationException; + import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageListener; -import javax.jms.MessageProducer; import javax.jms.Session; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.springframework.jms.listener.SessionAwareMessageListener; import org.springframework.jms.listener.SubscriptionNameProvider; -import org.springframework.jms.support.JmsUtils; -import org.springframework.jms.support.converter.MessageConversionException; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.converter.SimpleMessageConverter; -import org.springframework.jms.support.destination.DestinationResolver; -import org.springframework.jms.support.destination.DynamicDestinationResolver; import org.springframework.util.Assert; import org.springframework.util.MethodInvoker; import org.springframework.util.ObjectUtils; @@ -128,8 +119,8 @@ import org.springframework.util.ObjectUtils; * @see org.springframework.jms.listener.SessionAwareMessageListener * @see org.springframework.jms.listener.AbstractMessageListenerContainer#setMessageListener */ -public class MessageListenerAdapter - implements MessageListener, SessionAwareMessageListener, SubscriptionNameProvider { +public class MessageListenerAdapter extends AbstractAdaptableMessageListener + implements SubscriptionNameProvider { /** * Out-of-the-box value for the default listener method: "handleMessage". @@ -137,19 +128,10 @@ public class MessageListenerAdapter public static final String ORIGINAL_DEFAULT_LISTENER_METHOD = "handleMessage"; - /** Logger available to subclasses */ - protected final Log logger = LogFactory.getLog(getClass()); - private Object delegate; private String defaultListenerMethod = ORIGINAL_DEFAULT_LISTENER_METHOD; - private Object defaultResponseDestination; - - private DestinationResolver destinationResolver = new DynamicDestinationResolver(); - - private MessageConverter messageConverter; - /** * Create a new {@link MessageListenerAdapter} with default settings. @@ -205,112 +187,6 @@ public class MessageListenerAdapter return this.defaultListenerMethod; } - /** - * Set the default destination to send response messages to. This will be applied - * in case of a request message that does not carry a "JMSReplyTo" field. - *

Response destinations are only relevant for listener methods that return - * result objects, which will be wrapped in a response message and sent to a - * response destination. - *

Alternatively, specify a "defaultResponseQueueName" or "defaultResponseTopicName", - * to be dynamically resolved via the DestinationResolver. - * @see #setDefaultResponseQueueName(String) - * @see #setDefaultResponseTopicName(String) - * @see #getResponseDestination - */ - public void setDefaultResponseDestination(Destination destination) { - this.defaultResponseDestination = destination; - } - - /** - * Set the name of the default response queue to send response messages to. - * This will be applied in case of a request message that does not carry a - * "JMSReplyTo" field. - *

Alternatively, specify a JMS Destination object as "defaultResponseDestination". - * @see #setDestinationResolver - * @see #setDefaultResponseDestination(javax.jms.Destination) - */ - public void setDefaultResponseQueueName(String destinationName) { - this.defaultResponseDestination = new DestinationNameHolder(destinationName, false); - } - - /** - * Set the name of the default response topic to send response messages to. - * This will be applied in case of a request message that does not carry a - * "JMSReplyTo" field. - *

Alternatively, specify a JMS Destination object as "defaultResponseDestination". - * @see #setDestinationResolver - * @see #setDefaultResponseDestination(javax.jms.Destination) - */ - public void setDefaultResponseTopicName(String destinationName) { - this.defaultResponseDestination = new DestinationNameHolder(destinationName, true); - } - - /** - * Set the DestinationResolver that should be used to resolve response - * destination names for this adapter. - *

The default resolver is a DynamicDestinationResolver. Specify a - * JndiDestinationResolver for resolving destination names as JNDI locations. - * @see org.springframework.jms.support.destination.DynamicDestinationResolver - * @see org.springframework.jms.support.destination.JndiDestinationResolver - */ - public void setDestinationResolver(DestinationResolver destinationResolver) { - Assert.notNull(destinationResolver, "DestinationResolver must not be null"); - this.destinationResolver = destinationResolver; - } - - /** - * Return the DestinationResolver for this adapter. - */ - protected DestinationResolver getDestinationResolver() { - return this.destinationResolver; - } - - /** - * Set the converter that will convert incoming JMS messages to - * listener method arguments, and objects returned from listener - * methods back to JMS messages. - *

The default converter is a {@link SimpleMessageConverter}, which is able - * to handle {@link javax.jms.BytesMessage BytesMessages}, - * {@link javax.jms.TextMessage TextMessages} and - * {@link javax.jms.ObjectMessage ObjectMessages}. - */ - public void setMessageConverter(MessageConverter messageConverter) { - this.messageConverter = messageConverter; - } - - /** - * Return the converter that will convert incoming JMS messages to - * listener method arguments, and objects returned from listener - * methods back to JMS messages. - */ - protected MessageConverter getMessageConverter() { - return this.messageConverter; - } - - - /** - * Standard JMS {@link MessageListener} entry point. - *

Delegates the message to the target listener method, with appropriate - * conversion of the message argument. In case of an exception, the - * {@link #handleListenerException(Throwable)} method will be invoked. - *

Note: Does not support sending response messages based on - * result objects returned from listener methods. Use the - * {@link SessionAwareMessageListener} entry point (typically through a Spring - * message listener container) for handling result objects as well. - * @param message the incoming JMS message - * @see #handleListenerException - * @see #onMessage(javax.jms.Message, javax.jms.Session) - */ - @Override - public void onMessage(Message message) { - try { - onMessage(message, null); - } - catch (Throwable ex) { - handleListenerException(ex); - } - } - /** * Spring {@link SessionAwareMessageListener} entry point. *

Delegates the message to the target listener method, with appropriate @@ -374,44 +250,6 @@ public class MessageListenerAdapter } } - - /** - * Initialize the default implementations for the adapter's strategies. - * @see #setMessageConverter - * @see org.springframework.jms.support.converter.SimpleMessageConverter - */ - protected void initDefaultStrategies() { - setMessageConverter(new SimpleMessageConverter()); - } - - /** - * Handle the given exception that arose during listener execution. - * The default implementation logs the exception at error level. - *

This method only applies when used as standard JMS {@link MessageListener}. - * In case of the Spring {@link SessionAwareMessageListener} mechanism, - * exceptions get handled by the caller instead. - * @param ex the exception to handle - * @see #onMessage(javax.jms.Message) - */ - protected void handleListenerException(Throwable ex) { - logger.error("Listener execution failed", ex); - } - - /** - * Extract the message body from the given JMS message. - * @param message the JMS {@code Message} - * @return the content of the message, to be passed into the - * listener method as argument - * @throws JMSException if thrown by JMS API methods - */ - protected Object extractMessage(Message message) throws JMSException { - MessageConverter converter = getMessageConverter(); - if (converter != null) { - return converter.fromMessage(message); - } - return message; - } - /** * Determine the name of the listener method that is supposed to * handle the given message. @@ -481,176 +319,4 @@ public class MessageListenerAdapter } } - - /** - * Handle the given result object returned from the listener method, - * sending a response message back. - * @param result the result object to handle (never {@code null}) - * @param request the original request message - * @param session the JMS Session to operate on (may be {@code null}) - * @throws JMSException if thrown by JMS API methods - * @see #buildMessage - * @see #postProcessResponse - * @see #getResponseDestination - * @see #sendResponse - */ - protected void handleResult(Object result, Message request, Session session) throws JMSException { - if (session != null) { - if (logger.isDebugEnabled()) { - logger.debug("Listener method returned result [" + result + - "] - generating response message for it"); - } - Message response = buildMessage(session, result); - postProcessResponse(request, response); - Destination destination = getResponseDestination(request, response, session); - sendResponse(session, destination, response); - } - else { - if (logger.isWarnEnabled()) { - logger.warn("Listener method returned result [" + result + - "]: not generating response message for it because of no JMS Session given"); - } - } - } - - /** - * Build a JMS message to be sent as response based on the given result object. - * @param session the JMS Session to operate on - * @param result the content of the message, as returned from the listener method - * @return the JMS {@code Message} (never {@code null}) - * @throws JMSException if thrown by JMS API methods - * @see #setMessageConverter - */ - protected Message buildMessage(Session session, Object result) throws JMSException { - MessageConverter converter = getMessageConverter(); - if (converter != null) { - return converter.toMessage(result, session); - } - else { - if (!(result instanceof Message)) { - throw new MessageConversionException( - "No MessageConverter specified - cannot handle message [" + result + "]"); - } - return (Message) result; - } - } - - /** - * Post-process the given response message before it will be sent. - *

The default implementation sets the response's correlation id - * to the request message's correlation id, if any; otherwise to the - * request message id. - * @param request the original incoming JMS message - * @param response the outgoing JMS message about to be sent - * @throws JMSException if thrown by JMS API methods - * @see javax.jms.Message#setJMSCorrelationID - */ - protected void postProcessResponse(Message request, Message response) throws JMSException { - String correlation = request.getJMSCorrelationID(); - if (correlation == null) { - correlation = request.getJMSMessageID(); - } - response.setJMSCorrelationID(correlation); - } - - /** - * Determine a response destination for the given message. - *

The default implementation first checks the JMS Reply-To - * {@link Destination} of the supplied request; if that is not {@code null} - * it is returned; if it is {@code null}, then the configured - * {@link #resolveDefaultResponseDestination default response destination} - * is returned; if this too is {@code null}, then an - * {@link InvalidDestinationException} is thrown. - * @param request the original incoming JMS message - * @param response the outgoing JMS message about to be sent - * @param session the JMS Session to operate on - * @return the response destination (never {@code null}) - * @throws JMSException if thrown by JMS API methods - * @throws InvalidDestinationException if no {@link Destination} can be determined - * @see #setDefaultResponseDestination - * @see javax.jms.Message#getJMSReplyTo() - */ - protected Destination getResponseDestination(Message request, Message response, Session session) - throws JMSException { - - Destination replyTo = request.getJMSReplyTo(); - if (replyTo == null) { - replyTo = resolveDefaultResponseDestination(session); - if (replyTo == null) { - throw new InvalidDestinationException("Cannot determine response destination: " + - "Request message does not contain reply-to destination, and no default response destination set."); - } - } - return replyTo; - } - - /** - * Resolve the default response destination into a JMS {@link Destination}, using this - * accessor's {@link DestinationResolver} in case of a destination name. - * @return the located {@link Destination} - * @throws javax.jms.JMSException if resolution failed - * @see #setDefaultResponseDestination - * @see #setDefaultResponseQueueName - * @see #setDefaultResponseTopicName - * @see #setDestinationResolver - */ - protected Destination resolveDefaultResponseDestination(Session session) throws JMSException { - if (this.defaultResponseDestination instanceof Destination) { - return (Destination) this.defaultResponseDestination; - } - if (this.defaultResponseDestination instanceof DestinationNameHolder) { - DestinationNameHolder nameHolder = (DestinationNameHolder) this.defaultResponseDestination; - return getDestinationResolver().resolveDestinationName(session, nameHolder.name, nameHolder.isTopic); - } - return null; - } - - /** - * Send the given response message to the given destination. - * @param response the JMS message to send - * @param destination the JMS destination to send to - * @param session the JMS session to operate on - * @throws JMSException if thrown by JMS API methods - * @see #postProcessProducer - * @see javax.jms.Session#createProducer - * @see javax.jms.MessageProducer#send - */ - protected void sendResponse(Session session, Destination destination, Message response) throws JMSException { - MessageProducer producer = session.createProducer(destination); - try { - postProcessProducer(producer, response); - producer.send(response); - } - finally { - JmsUtils.closeMessageProducer(producer); - } - } - - /** - * Post-process the given message producer before using it to send the response. - *

The default implementation is empty. - * @param producer the JMS message producer that will be used to send the message - * @param response the outgoing JMS message about to be sent - * @throws JMSException if thrown by JMS API methods - */ - protected void postProcessProducer(MessageProducer producer, Message response) throws JMSException { - } - - - /** - * Internal class combining a destination name - * and its target destination type (queue or topic). - */ - private static class DestinationNameHolder { - - public final String name; - - public final boolean isTopic; - - public DestinationNameHolder(String name, boolean isTopic) { - this.name = name; - this.isTopic = isTopic; - } - } - } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java new file mode 100644 index 0000000000..f95a9ca14e --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.listener.adapter; + +import java.util.Map; + +import javax.jms.JMSException; +import javax.jms.Session; + +import org.springframework.jms.support.converter.JmsHeaderMapper; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessagingException; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.messaging.support.MessageBuilder; + +/** + * A {@link javax.jms.MessageListener} adapter that invokes a configurable + * {@link InvocableHandlerMethod}. + * + *

Wraps the incoming {@link javax.jms.Message} to Spring's {@link Message} + * abstraction, copying the JMS standard headers using a configurable + * {@link JmsHeaderMapper}. + * + *

The original {@link javax.jms.Message} and the {@link javax.jms.Session} + * are provided as additional arguments so that these can be injected as + * method arguments if necessary. + * + * @author Stephane Nicoll + * @since 4.1 + * @see Message + * @see JmsHeaderMapper + * @see InvocableHandlerMethod + */ +public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener { + + private InvocableHandlerMethod handlerMethod; + + + /** + * Set the {@link InvocableHandlerMethod} to use to invoke the method + * processing an incoming {@link javax.jms.Message}. + */ + public void setHandlerMethod(InvocableHandlerMethod handlerMethod) { + this.handlerMethod = handlerMethod; + } + + @Override + public void onMessage(javax.jms.Message jmsMessage, Session session) throws JMSException { + try { + Message message = toMessagingMessage(jmsMessage); + if (logger.isDebugEnabled()) { + logger.debug("Processing [" + message + "]"); + } + Object result = handlerMethod.invoke(message, jmsMessage, session); + if (result != null) { + handleResult(result, jmsMessage, session); + } + else { + logger.trace("No result object given - no result to handle"); + } + } + catch (MessagingException e) { + throw new ListenerExecutionFailedException(createMessagingErrorMessage("Listener method could not " + + "be invoked with the incoming message"), e); + } + catch (Exception e) { + throw new ListenerExecutionFailedException("Listener method '" + + handlerMethod.getMethod().toGenericString() + "' threw exception", e); + } + } + + @SuppressWarnings("unchecked") + protected Message toMessagingMessage(javax.jms.Message jmsMessage) throws JMSException { + Map mappedHeaders = getHeaderMapper().toHeaders(jmsMessage); + Object convertedObject = extractMessage(jmsMessage); + MessageBuilder builder = (convertedObject instanceof org.springframework.messaging.Message) ? + MessageBuilder.fromMessage((org.springframework.messaging.Message) convertedObject) : + MessageBuilder.withPayload(convertedObject); + return builder.copyHeadersIfAbsent(mappedHeaders).build(); + } + + private String createMessagingErrorMessage(String description) { + StringBuilder sb = new StringBuilder(description).append("\n") + .append("Endpoint handler details:\n") + .append("Method [").append(handlerMethod.getMethod()).append("]\n") + .append("Bean [").append(handlerMethod.getBean()).append("]\n"); + return sb.toString(); + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java index 637790da50..5a46199288 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2014 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. @@ -19,6 +19,7 @@ package org.springframework.jms.listener.endpoint; import javax.jms.Session; import org.springframework.core.Constants; +import org.springframework.jms.support.converter.MessageConverter; /** * Common configuration object for activating a JMS message endpoint. @@ -29,6 +30,7 @@ import org.springframework.core.Constants; * but not tied to it. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.5 * @see JmsActivationSpecFactory * @see JmsMessageEndpointManager#setActivationSpecConfig @@ -58,6 +60,8 @@ public class JmsActivationSpecConfig { private int prefetchSize = -1; + private MessageConverter messageConverter; + public void setDestinationName(String destinationName) { this.destinationName = destinationName; @@ -201,4 +205,19 @@ public class JmsActivationSpecConfig { return this.prefetchSize; } + /** + * Set the {@link MessageConverter} strategy for converting JMS Messages. + * @param messageConverter the message converter to use + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + /** + * Return the {@link MessageConverter} to use, if any. + */ + public MessageConverter getMessageConverter() { + return messageConverter; + } + } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java index 1a86567d0a..fe448aa2bc 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -39,6 +39,7 @@ import org.springframework.jca.endpoint.AbstractMessageEndpointFactory; * {@link javax.resource.spi.ResourceAdapter} instance. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.5 * @see #setMessageListener * @see #setTransactionManager @@ -56,6 +57,13 @@ public class JmsMessageEndpointFactory extends AbstractMessageEndpointFactory { this.messageListener = messageListener; } + /** + * Return the JMS MessageListener for this endpoint. + */ + protected MessageListener getMessageListener() { + return messageListener; + } + /** * Creates a concrete JMS message endpoint, internal to this factory. */ diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java index cbf5f37654..77a9716f28 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -21,6 +21,8 @@ import javax.resource.ResourceException; import org.springframework.beans.factory.BeanNameAware; import org.springframework.jca.endpoint.GenericMessageEndpointManager; +import org.springframework.jms.listener.MessageListenerContainer; +import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.destination.DestinationResolver; /** @@ -39,6 +41,7 @@ import org.springframework.jms.support.destination.DestinationResolver; * for obtaining the current JMS {@link javax.jms.Session}. * * @author Juergen Hoeller + * @author Stephane Nicoll * @since 2.5 * @see javax.jms.MessageListener * @see #setActivationSpecConfig @@ -46,7 +49,8 @@ import org.springframework.jms.support.destination.DestinationResolver; * @see JmsActivationSpecFactory * @see JmsMessageEndpointFactory */ -public class JmsMessageEndpointManager extends GenericMessageEndpointManager implements BeanNameAware { +public class JmsMessageEndpointManager extends GenericMessageEndpointManager + implements BeanNameAware, MessageListenerContainer { private final JmsMessageEndpointFactory endpointFactory = new JmsMessageEndpointFactory(); @@ -67,6 +71,13 @@ public class JmsMessageEndpointManager extends GenericMessageEndpointManager imp this.messageListenerSet = true; } + /** + * Return the JMS MessageListener for this endpoint. + */ + public MessageListener getMessageListener() { + return this.endpointFactory.getMessageListener(); + } + /** * Set the XA transaction manager to use for wrapping endpoint * invocations, enlisting the endpoint resource in each such transaction. @@ -128,6 +139,23 @@ public class JmsMessageEndpointManager extends GenericMessageEndpointManager imp this.activationSpecConfig = activationSpecConfig; } + /** + * Return the {@link JmsActivationSpecConfig} object that this endpoint manager + * should use for activating its listener. Return {@code null} if none is set. + */ + public JmsActivationSpecConfig getActivationSpecConfig() { + return activationSpecConfig; + } + + @Override + public MessageConverter getMessageConverter() { + JmsActivationSpecConfig config = getActivationSpecConfig(); + if (config != null) { + return config.getMessageConverter(); + } + return null; + } + /** * Set the name of this message endpoint. Populated with the bean name * automatically when defined within Spring's bean factory. @@ -137,6 +165,18 @@ public class JmsMessageEndpointManager extends GenericMessageEndpointManager imp this.endpointFactory.setBeanName(beanName); } + @Override + public void setupMessageListener(Object messageListener) { + if (messageListener instanceof MessageListener) { + setMessageListener((MessageListener) messageListener); + } + else { + throw new IllegalArgumentException("Unsupported message listener '" + + messageListener.getClass().getName() + "': only '" + + MessageListener.class.getName() + "' type is supported"); + } + } + @Override public void afterPropertiesSet() throws ResourceException { if (this.messageListenerSet) { diff --git a/spring-jms/src/main/java/org/springframework/jms/support/JmsMessageHeaderAccessor.java b/spring-jms/src/main/java/org/springframework/jms/support/JmsMessageHeaderAccessor.java new file mode 100644 index 0000000000..0261b7131c --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/support/JmsMessageHeaderAccessor.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.support; + +import java.util.List; +import java.util.Map; + +import javax.jms.Destination; + +import org.springframework.jms.support.converter.JmsHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.NativeMessageHeaderAccessor; + +/** + * A {@link org.springframework.messaging.support.MessageHeaderAccessor} + * implementation giving access to JMS-specific headers. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class JmsMessageHeaderAccessor extends NativeMessageHeaderAccessor { + + protected JmsMessageHeaderAccessor(Map> nativeHeaders) { + super(nativeHeaders); + } + + protected JmsMessageHeaderAccessor(Message message) { + super(message); + } + + + /** + * Create {@link JmsMessageHeaderAccessor} from the headers of an existing message. + */ + public static JmsMessageHeaderAccessor wrap(Message message) { + return new JmsMessageHeaderAccessor(message); + } + + + @Override + public Object getReplyChannel() { + return getReplyTo(); + } + + public String getCorrelationId() { + return (String) getHeader(JmsHeaders.CORRELATION_ID); + } + + public Destination getDestination() { + return (Destination) getHeader(JmsHeaders.DESTINATION); + } + + public Integer getDeliveryMode() { + return (Integer) getHeader(JmsHeaders.DELIVERY_MODE); + } + + public Long getExpiration() { + return (Long) getHeader(JmsHeaders.EXPIRATION); + } + + public String getMessageId() { + return (String) getHeader(JmsHeaders.MESSAGE_ID); + } + + public Integer getPriority() { + return (Integer) getHeader(JmsHeaders.PRIORITY); + } + + public Destination getReplyTo() { + return (Destination) getHeader(JmsHeaders.REPLY_TO); + } + + public Boolean getRedelivered() { + return (Boolean) getHeader(JmsHeaders.REDELIVERED); + } + + public String getType() { + return (String) getHeader(JmsHeaders.TYPE); + } + + public Long getTimestamp() { + return (Long) getHeader(JmsHeaders.TIMESTAMP); + } + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/JmsHeaderMapper.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/JmsHeaderMapper.java new file mode 100644 index 0000000000..88d790c263 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/JmsHeaderMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.support.converter; + +import javax.jms.Message; + +import org.springframework.messaging.mapping.HeaderMapper; + +/** + * Strategy interface for mapping messaging Message headers to an outbound + * JMS Message (e.g. to configure JMS properties) or extracting messaging + * header values from an inbound JMS Message. + * + * @author Mark Fisher + * @author Oleg Zhurakousky + * @author Gary Russell + * @since 4.1 + */ +public interface JmsHeaderMapper extends HeaderMapper { + + static final String CONTENT_TYPE_PROPERTY = "content_type"; + +} + diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/JmsHeaders.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/JmsHeaders.java new file mode 100644 index 0000000000..f15d992f79 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/JmsHeaders.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.support.converter; + +/** + * Pre-defined names and prefixes to be used for setting and/or retrieving JMS + * attributes from/to integration Message Headers. + * + * @author Mark Fisher + * @author Stephane Nicoll + * @since 4.1 + */ +public interface JmsHeaders { + + /** + * Prefix used for JMS API related headers in order to distinguish from + * user-defined headers and other internal headers (e.g. correlationId). + * @see SimpleJmsHeaderMapper + */ + public static final String PREFIX = "jms_"; + + /** + * Correlation ID for the message. This may be the {@link #MESSAGE_ID} of + * the message that this message replies to. It may also be an + * application-specific identifier. + * @see javax.jms.Message#getJMSCorrelationID() + */ + public static final String CORRELATION_ID = PREFIX + "correlationId"; + + /** + * Name of the destination (topic or queue) of the message. + *

Read only value. + * @see javax.jms.Message#getJMSDestination() + * @see javax.jms.Destination + * @see javax.jms.Queue + * @see javax.jms.Topic + */ + public static final String DESTINATION = PREFIX + "destination"; + + /** + * Distribution mode. + *

Read only value. + * @see javax.jms.Message#getJMSDeliveryMode() + * @see javax.jms.DeliveryMode + */ + public static final String DELIVERY_MODE = PREFIX + "deliveryMode"; + + /** + * Message expiration date and time. + *

Read only value. + * @see javax.jms.Message#getJMSExpiration() + */ + public static final String EXPIRATION = PREFIX + "expiration"; + + /** + * Unique Identifier for a message. + *

Read only value. + * @see javax.jms.Message#getJMSMessageID() + */ + public static final String MESSAGE_ID = PREFIX + "messageId"; + + /** + * The message priority level. + *

Read only value. + * @see javax.jms.Message#getJMSPriority() + */ + public static final String PRIORITY = PREFIX + "priority"; + + /** + * Name of the destination (topic or queue) the message replies should + * be sent to. + * @see javax.jms.Message#getJMSReplyTo() + */ + public static final String REPLY_TO = PREFIX + "replyTo"; + + /** + * Specify if the message was resent. This occurs when a message + * consumer fails to acknowledge the message reception. + *

Read only value. + * @see javax.jms.Message#getJMSRedelivered() + */ + public static final String REDELIVERED = PREFIX + "redelivered"; + + /** + * Message type label. This type is a string value describing the message + * in a functional manner. + * @see javax.jms.Message#getJMSType() + */ + public static final String TYPE = PREFIX + "type"; + + /** + * Date and time of the message sending operation. + *

Read only value. + * @see javax.jms.Message#getJMSTimestamp() + */ + public static final String TIMESTAMP = PREFIX + "timestamp"; + +} diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/SimpleJmsHeaderMapper.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/SimpleJmsHeaderMapper.java new file mode 100644 index 0000000000..2db91c2dd8 --- /dev/null +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/SimpleJmsHeaderMapper.java @@ -0,0 +1,300 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.support.converter; + +import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.jms.Destination; +import javax.jms.JMSException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.messaging.MessageHeaders; +import org.springframework.util.StringUtils; + +/** + * Simple implementation of {@link JmsHeaderMapper}. + *

+ * This implementation copies JMS API headers (e.g. JMSReplyTo) to and from + * {@link org.springframework.messaging.Message Messages}. Any user-defined + * properties will also be copied from a JMS Message to a Message, and any + * other headers on a Message (beyond the JMS API headers) will likewise + * be copied to a JMS Message. Those other headers will be copied to the + * general properties of a JMS Message whereas the JMS API headers are passed + * to the appropriate setter methods (e.g. setJMSReplyTo). + *

+ * Constants for the JMS API headers are defined in {@link JmsHeaders}. + * Note that most of the JMS headers are read-only: the JMSDestination, + * JMSDeliveryMode, JMSExpiration, JMSMessageID, JMSPriority, JMSRedelivered + * and JMSTimestamp flags are only copied from a JMS Message. Those + * values will not be passed along from a Message to an outbound + * JMS Message. + * + * @author Mark Fisher + * @author Gary Russel + * @since 4.1 + */ +public class SimpleJmsHeaderMapper implements JmsHeaderMapper { + + private static List> SUPPORTED_PROPERTY_TYPES = Arrays.asList(new Class[] { + Boolean.class, Byte.class, Double.class, Float.class, Integer.class, Long.class, Short.class, String.class}); + + + private final Log logger = LogFactory.getLog(this.getClass()); + + private volatile String inboundPrefix = ""; + + private volatile String outboundPrefix = ""; + + /** + * Specify a prefix to be appended to the message header name for any + * JMS property that is being mapped into the MessageHeaders. The + * default is an empty string (no prefix). + *

+ * This does not affect the JMS properties covered by the specification/API, + * such as JMSCorrelationID, etc. The header names used for mapping such + * properties are all defined in our {@link JmsHeaders}. + * + * @param inboundPrefix The inbound prefix. + */ + public void setInboundPrefix(String inboundPrefix) { + this.inboundPrefix = (inboundPrefix != null) ? inboundPrefix : ""; + } + + /** + * Specify a prefix to be appended to the JMS property name for any + * message header that is being mapped into the JMS Message. The + * default is an empty string (no prefix). + *

+ * This does not affect the JMS properties covered by the specification/API, + * such as JMSCorrelationID, etc. The header names used for mapping such + * properties are all defined in our {@link JmsHeaders}. + * + * @param outboundPrefix The outbound prefix. + */ + public void setOutboundPrefix(String outboundPrefix) { + this.outboundPrefix = (outboundPrefix != null) ? outboundPrefix : ""; + } + + @Override + public void fromHeaders(MessageHeaders headers, javax.jms.Message jmsMessage) { + try { + Object jmsCorrelationId = headers.get(JmsHeaders.CORRELATION_ID); + if (jmsCorrelationId instanceof Number) { + jmsCorrelationId = jmsCorrelationId.toString(); + } + if (jmsCorrelationId instanceof String) { + try { + jmsMessage.setJMSCorrelationID((String) jmsCorrelationId); + } + catch (Exception e) { + logger.info("failed to set JMSCorrelationID, skipping", e); + } + } + Object jmsReplyTo = headers.get(JmsHeaders.REPLY_TO); + if (jmsReplyTo instanceof Destination) { + try { + jmsMessage.setJMSReplyTo((Destination) jmsReplyTo); + } + catch (Exception e) { + logger.info("failed to set JMSReplyTo, skipping", e); + } + } + Object jmsType = headers.get(JmsHeaders.TYPE); + if (jmsType instanceof String) { + try { + jmsMessage.setJMSType((String) jmsType); + } + catch (Exception e) { + logger.info("failed to set JMSType, skipping", e); + } + } + Set headerNames = headers.keySet(); + for (String headerName : headerNames) { + if (StringUtils.hasText(headerName) && !headerName.startsWith(JmsHeaders.PREFIX)) { + Object value = headers.get(headerName); + if (value != null && SUPPORTED_PROPERTY_TYPES.contains(value.getClass())) { + try { + String propertyName = this.fromHeaderName(headerName); + jmsMessage.setObjectProperty(propertyName, value); + } + catch (Exception e) { + if (headerName.startsWith("JMSX")) { + if (logger.isTraceEnabled()) { + logger.trace("skipping reserved header, it cannot be set by client: " + headerName); + } + } + else if (logger.isWarnEnabled()) { + logger.warn("failed to map Message header '" + headerName + "' to JMS property", e); + } + } + } + } + } + } + catch (Exception e) { + if (logger.isWarnEnabled()) { + logger.warn("error occurred while mapping from MessageHeaders to JMS properties", e); + } + } + } + + @Override + public Map toHeaders(javax.jms.Message jmsMessage) { + Map headers = new HashMap(); + try { + try { + String correlationId = jmsMessage.getJMSCorrelationID(); + if (correlationId != null) { + headers.put(JmsHeaders.CORRELATION_ID, correlationId); + } + } + catch (Exception e) { + logger.info("failed to read JMSCorrelationID property, skipping", e); + } + try { + Destination destination = jmsMessage.getJMSDestination(); + if (destination != null) { + headers.put(JmsHeaders.DESTINATION, destination); + } + } + catch (Exception e) { + logger.info("failed to read JMSDestination property, skipping", e); + } + try { + int deliveryMode = jmsMessage.getJMSDeliveryMode(); + headers.put(JmsHeaders.DELIVERY_MODE, deliveryMode); + } + catch (Exception e) { + logger.info("failed to read JMSDeliveryMode property, skipping", e); + } + try { + long expiration = jmsMessage.getJMSExpiration(); + headers.put(JmsHeaders.EXPIRATION, expiration); + } + catch (Exception e) { + logger.info("failed to read JMSExpiration property, skipping", e); + } + try { + String messageId = jmsMessage.getJMSMessageID(); + if (messageId != null) { + headers.put(JmsHeaders.MESSAGE_ID, messageId); + } + } + catch (Exception e) { + logger.info("failed to read JMSMessageID property, skipping", e); + } + try { + headers.put(JmsHeaders.PRIORITY, jmsMessage.getJMSPriority()); + } + catch (Exception e) { + logger.info("failed to read JMSPriority property, skipping", e); + } + try { + Destination replyTo = jmsMessage.getJMSReplyTo(); + if (replyTo != null) { + headers.put(JmsHeaders.REPLY_TO, replyTo); + } + } + catch (Exception e) { + logger.info("failed to read JMSReplyTo property, skipping", e); + } + try { + headers.put(JmsHeaders.REDELIVERED, jmsMessage.getJMSRedelivered()); + } + catch (Exception e) { + logger.info("failed to read JMSRedelivered property, skipping", e); + } + try { + String type = jmsMessage.getJMSType(); + if (type != null) { + headers.put(JmsHeaders.TYPE, type); + } + } + catch (Exception e) { + logger.info("failed to read JMSType property, skipping", e); + } + try { + headers.put(JmsHeaders.TIMESTAMP, jmsMessage.getJMSTimestamp()); + } + catch (Exception e) { + logger.info("failed to read JMSTimestamp property, skipping", e); + } + + + Enumeration jmsPropertyNames = jmsMessage.getPropertyNames(); + if (jmsPropertyNames != null) { + while (jmsPropertyNames.hasMoreElements()) { + String propertyName = jmsPropertyNames.nextElement().toString(); + try { + String headerName = this.toHeaderName(propertyName); + headers.put(headerName, jmsMessage.getObjectProperty(propertyName)); + } + catch (Exception e) { + if (logger.isWarnEnabled()) { + logger.warn("error occurred while mapping JMS property '" + + propertyName + "' to Message header", e); + } + } + } + } + } + catch (JMSException e) { + if (logger.isWarnEnabled()) { + logger.warn("error occurred while mapping from JMS properties to MessageHeaders", e); + } + } + return headers; + } + + /** + * Add the outbound prefix if necessary. + *

Convert {@link MessageHeaders#CONTENT_TYPE} to content_type for JMS compliance. + */ + private String fromHeaderName(String headerName) { + String propertyName = headerName; + if (StringUtils.hasText(this.outboundPrefix) && !propertyName.startsWith(this.outboundPrefix)) { + propertyName = this.outboundPrefix + headerName; + } + else if (MessageHeaders.CONTENT_TYPE.equals(headerName)) { + propertyName = CONTENT_TYPE_PROPERTY; + } + return propertyName; + } + + /** + * Add the inbound prefix if necessary. + *

Convert content_type to {@link MessageHeaders#CONTENT_TYPE}. + */ + private String toHeaderName(String propertyName) { + String headerName = propertyName; + if (StringUtils.hasText(this.inboundPrefix) && !headerName.startsWith(this.inboundPrefix)) { + headerName = this.inboundPrefix + propertyName; + } + else if (CONTENT_TYPE_PROPERTY.equals(propertyName)) { + headerName = MessageHeaders.CONTENT_TYPE; + } + return headerName; + } + +} diff --git a/spring-jms/src/main/resources/META-INF/spring.schemas b/spring-jms/src/main/resources/META-INF/spring.schemas index 0b3f016cdc..c7c42d4642 100644 --- a/spring-jms/src/main/resources/META-INF/spring.schemas +++ b/spring-jms/src/main/resources/META-INF/spring.schemas @@ -3,4 +3,5 @@ http\://www.springframework.org/schema/jms/spring-jms-3.0.xsd=org/springframewor http\://www.springframework.org/schema/jms/spring-jms-3.1.xsd=org/springframework/jms/config/spring-jms-3.1.xsd http\://www.springframework.org/schema/jms/spring-jms-3.2.xsd=org/springframework/jms/config/spring-jms-3.2.xsd http\://www.springframework.org/schema/jms/spring-jms-4.0.xsd=org/springframework/jms/config/spring-jms-4.0.xsd -http\://www.springframework.org/schema/jms/spring-jms.xsd=org/springframework/jms/config/spring-jms-4.0.xsd +http\://www.springframework.org/schema/jms/spring-jms-4.1.xsd=org/springframework/jms/config/spring-jms-4.1.xsd +http\://www.springframework.org/schema/jms/spring-jms.xsd=org/springframework/jms/config/spring-jms-4.1.xsd diff --git a/spring-jms/src/main/resources/org/springframework/jms/config/spring-jms-4.1.xsd b/spring-jms/src/main/resources/org/springframework/jms/config/spring-jms-4.1.xsd new file mode 100644 index 0000000000..582cd48ca6 --- /dev/null +++ b/spring-jms/src/main/resources/org/springframework/jms/config/spring-jms-4.1.xsd @@ -0,0 +1,575 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-jms/src/test/java/org/springframework/jms/StubTextMessage.java b/spring-jms/src/test/java/org/springframework/jms/StubTextMessage.java new file mode 100644 index 0000000000..965d4707a3 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/StubTextMessage.java @@ -0,0 +1,264 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms; + +import java.util.Enumeration; +import java.util.concurrent.ConcurrentHashMap; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.TextMessage; + +/** + * Stub JMS Message implementation intended for testing purposes only. + * + * @author Mark Fisher + * @since 4.1 + */ +public class StubTextMessage implements TextMessage { + + private String messageId; + + private String text; + + private int deliveryMode = DEFAULT_DELIVERY_MODE; + + private Destination destination; + + private String correlationId; + + private Destination replyTo; + + private String type; + + private long timestamp = 0L; + + private long expiration = 0L; + + private int priority = DEFAULT_PRIORITY; + + private boolean redelivered; + + private ConcurrentHashMap properties = new ConcurrentHashMap(); + + + public StubTextMessage() { + } + + public StubTextMessage(String text) { + this.text = text; + } + + + public String getText() throws JMSException { + return this.text; + } + + public void setText(String text) throws JMSException { + this.text = text; + } + + public void acknowledge() throws JMSException { + throw new UnsupportedOperationException(); + } + + public void clearBody() throws JMSException { + this.text = null; + } + + public void clearProperties() throws JMSException { + this.properties.clear(); + } + + public boolean getBooleanProperty(String name) throws JMSException { + Object value = this.properties.get(name); + return (value instanceof Boolean) ? ((Boolean) value).booleanValue() : false; + } + + public byte getByteProperty(String name) throws JMSException { + Object value = this.properties.get(name); + return (value instanceof Byte) ? ((Byte) value).byteValue() : 0; + } + + public double getDoubleProperty(String name) throws JMSException { + Object value = this.properties.get(name); + return (value instanceof Double) ? ((Double) value).doubleValue() : 0; + } + + public float getFloatProperty(String name) throws JMSException { + Object value = this.properties.get(name); + return (value instanceof Float) ? ((Float) value).floatValue() : 0; + } + + public int getIntProperty(String name) throws JMSException { + Object value = this.properties.get(name); + return (value instanceof Integer) ? ((Integer) value).intValue() : 0; + } + + public String getJMSCorrelationID() throws JMSException { + return this.correlationId; + } + + public byte[] getJMSCorrelationIDAsBytes() throws JMSException { + return this.correlationId.getBytes(); + } + + public int getJMSDeliveryMode() throws JMSException { + return this.deliveryMode; + } + + public Destination getJMSDestination() throws JMSException { + return this.destination; + } + + public long getJMSExpiration() throws JMSException { + return this.expiration; + } + + public String getJMSMessageID() throws JMSException { + return this.messageId; + } + + public int getJMSPriority() throws JMSException { + return this.priority; + } + + public boolean getJMSRedelivered() throws JMSException { + return this.redelivered; + } + + public Destination getJMSReplyTo() throws JMSException { + return this.replyTo; + } + + public long getJMSTimestamp() throws JMSException { + return this.timestamp; + } + + public String getJMSType() throws JMSException { + return this.type; + } + + public long getLongProperty(String name) throws JMSException { + Object value = this.properties.get(name); + return (value instanceof Long) ? ((Long) value).longValue() : 0; + } + + public Object getObjectProperty(String name) throws JMSException { + return this.properties.get(name); + } + + public Enumeration getPropertyNames() throws JMSException { + return this.properties.keys(); + } + + public short getShortProperty(String name) throws JMSException { + Object value = this.properties.get(name); + return (value instanceof Short) ? ((Short) value).shortValue() : 0; + } + + public String getStringProperty(String name) throws JMSException { + Object value = this.properties.get(name); + return (value instanceof String) ? (String) value : null; + } + + public boolean propertyExists(String name) throws JMSException { + return this.properties.containsKey(name); + } + + public void setBooleanProperty(String name, boolean value) throws JMSException { + this.properties.put(name, value); + } + + public void setByteProperty(String name, byte value) throws JMSException { + this.properties.put(name, value); + } + + public void setDoubleProperty(String name, double value) throws JMSException { + this.properties.put(name, value); + } + + public void setFloatProperty(String name, float value) throws JMSException { + this.properties.put(name, value); + } + + public void setIntProperty(String name, int value) throws JMSException { + this.properties.put(name, value); + } + + public void setJMSCorrelationID(String correlationId) throws JMSException { + this.correlationId = correlationId; + } + + public void setJMSCorrelationIDAsBytes(byte[] correlationID) throws JMSException { + this.correlationId = new String(correlationID); + } + + public void setJMSDeliveryMode(int deliveryMode) throws JMSException { + this.deliveryMode = deliveryMode; + } + + public void setJMSDestination(Destination destination) throws JMSException { + this.destination = destination; + } + + public void setJMSExpiration(long expiration) throws JMSException { + this.expiration = expiration; + } + + public void setJMSMessageID(String id) throws JMSException { + this.messageId = id; + } + + public void setJMSPriority(int priority) throws JMSException { + this.priority = priority; + } + + public void setJMSRedelivered(boolean redelivered) throws JMSException { + this.redelivered = redelivered; + } + + public void setJMSReplyTo(Destination replyTo) throws JMSException { + this.replyTo = replyTo; + } + + public void setJMSTimestamp(long timestamp) throws JMSException { + this.timestamp = timestamp; + } + + public void setJMSType(String type) throws JMSException { + this.type = type; + } + + public void setLongProperty(String name, long value) throws JMSException { + this.properties.put(name, value); + } + + public void setObjectProperty(String name, Object value) throws JMSException { + this.properties.put(name, value); + } + + public void setShortProperty(String name, short value) throws JMSException { + this.properties.put(name, value); + } + + public void setStringProperty(String name, String value) throws JMSException { + this.properties.put(name, value); + } + +} + diff --git a/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java b/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java new file mode 100644 index 0000000000..45016184a8 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java @@ -0,0 +1,210 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.annotation; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import javax.jms.JMSException; +import javax.jms.Session; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.context.ApplicationContext; +import org.springframework.jms.StubTextMessage; +import org.springframework.jms.config.JmsListenerContainerTestFactory; +import org.springframework.jms.config.JmsListenerEndpoint; +import org.springframework.jms.config.JmsListenerEndpointRegistry; +import org.springframework.jms.config.MethodJmsListenerEndpoint; +import org.springframework.jms.config.SimpleJmsListenerEndpoint; +import org.springframework.jms.listener.SimpleMessageListenerContainer; +import org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; + +/** + * + * @author Stephane Nicoll + */ +public abstract class AbstractJmsAnnotationDrivenTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public abstract void sampleConfiguration(); + + @Test + public abstract void fullConfiguration(); + + @Test + public abstract void customConfiguration(); + + @Test + public abstract void defaultContainerFactoryConfiguration(); + + @Test + public abstract void jmsHandlerMethodFactoryConfiguration() throws JMSException; + + /** + * Test for {@link SampleBean} discovery. + */ + public void testSampleConfiguration(ApplicationContext context) { + JmsListenerContainerTestFactory defaultFactory = + context.getBean("defaultFactory", JmsListenerContainerTestFactory.class); + JmsListenerContainerTestFactory simpleFactory = + context.getBean("simpleFactory", JmsListenerContainerTestFactory.class); + assertEquals(1, defaultFactory.getContainers().size()); + assertEquals(1, simpleFactory.getContainers().size()); + } + + @Component + static class SampleBean { + + @JmsListener(containerFactory = "defaultFactory", destination = "myQueue") + public void defaultHandle(String msg) { + } + + @JmsListener(containerFactory = "simpleFactory", destination = "myQueue") + public void simpleHandle(String msg) { + } + } + + /** + * Test for {@link FullBean} discovery. + */ + public void testFullConfiguration(ApplicationContext context) { + JmsListenerContainerTestFactory simpleFactory = + context.getBean("simpleFactory", JmsListenerContainerTestFactory.class); + assertEquals(1, simpleFactory.getContainers().size()); + MethodJmsListenerEndpoint endpoint = (MethodJmsListenerEndpoint) + simpleFactory.getContainers().get(0).getEndpoint(); + assertEquals("listener1", endpoint.getId()); + assertEquals("queueIn", endpoint.getDestination()); + assertTrue(endpoint.isQueue()); + assertEquals("mySelector", endpoint.getSelector()); + assertEquals("mySubscription", endpoint.getSubscription()); + } + + @Component + static class FullBean { + + @JmsListener(id = "listener1", containerFactory = "simpleFactory", destination = "queueIn", + responseDestination = "queueOut", selector = "mySelector", subscription = "mySubscription") + public String fullHandle(String msg) { + return "reply"; + } + } + + /** + * Test for {@link CustomBean} and an manually endpoint registered + * with "myCustomEndpointId". + */ + public void testCustomConfiguration(ApplicationContext context) { + JmsListenerContainerTestFactory defaultFactory = + context.getBean("defaultFactory", JmsListenerContainerTestFactory.class); + JmsListenerContainerTestFactory customFactory = + context.getBean("customFactory", JmsListenerContainerTestFactory.class); + assertEquals(1, defaultFactory.getContainers().size()); + assertEquals(1, customFactory.getContainers().size()); + JmsListenerEndpoint endpoint = defaultFactory.getContainers().get(0).getEndpoint(); + assertEquals("Wrong endpoint type", SimpleJmsListenerEndpoint.class, endpoint.getClass()); + assertEquals("Wrong listener set in custom endpoint", context.getBean("simpleMessageListener"), + ((SimpleJmsListenerEndpoint) endpoint).getMessageListener()); + + JmsListenerEndpointRegistry customRegistry = + context.getBean("customRegistry", JmsListenerEndpointRegistry.class); + assertEquals("Wrong number of containers in the registry", 2, + customRegistry.getContainers().size()); + assertNotNull("Container with custom id on the annotation should be found", + customRegistry.getContainer("listenerId")); + assertNotNull("Container created with custom id should be found", + customRegistry.getContainer("myCustomEndpointId")); + } + + @Component + static class CustomBean { + + @JmsListener(id = "listenerId", containerFactory = "customFactory", destination = "myQueue") + public void customHandle(String msg) { + } + } + + /** + * Test for {@link DefaultBean} that does not define the container + * factory to use as a default is registered. + */ + public void testDefaultContainerFactoryConfiguration(ApplicationContext context) { + JmsListenerContainerTestFactory defaultFactory = + context.getBean("defaultFactory", JmsListenerContainerTestFactory.class); + assertEquals(1, defaultFactory.getContainers().size()); + } + + static class DefaultBean { + + @JmsListener(destination = "myQueue") + public void handleIt(String msg) { + } + } + + /** + * Test for {@link ValidationBean} with a validator ({@link TestValidator}) specified + * in a custom {@link org.springframework.jms.config.DefaultJmsHandlerMethodFactory}. + * + * The test should throw a {@link org.springframework.jms.listener.adapter.ListenerExecutionFailedException} + */ + public void testJmsHandlerMethodFactoryConfiguration(ApplicationContext context) throws JMSException { + JmsListenerContainerTestFactory simpleFactory = + context.getBean("defaultFactory", JmsListenerContainerTestFactory.class); + assertEquals(1, simpleFactory.getContainers().size()); + MethodJmsListenerEndpoint endpoint = (MethodJmsListenerEndpoint) + simpleFactory.getContainers().get(0).getEndpoint(); + + SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + endpoint.setupMessageContainer(container); + MessagingMessageListenerAdapter listener = (MessagingMessageListenerAdapter) container.getMessageListener(); + listener.onMessage(new StubTextMessage("failValidation"), mock(Session.class)); + } + + @Component + static class ValidationBean { + + @JmsListener(containerFactory = "defaultFactory", destination = "myQueue") + public void defaultHandle(@Validated String msg) { + } + } + + static class TestValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return String.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + String value = (String) target; + if ("failValidation".equals(value)) { + errors.reject("TEST: expected invalid value"); + } + } + } +} diff --git a/spring-jms/src/test/java/org/springframework/jms/annotation/AnnotationDrivenNamespaceTests.java b/spring-jms/src/test/java/org/springframework/jms/annotation/AnnotationDrivenNamespaceTests.java new file mode 100644 index 0000000000..7bfe46abab --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/annotation/AnnotationDrivenNamespaceTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.annotation; + +import javax.jms.JMSException; +import javax.jms.MessageListener; + +import org.hamcrest.core.Is; +import org.junit.Test; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.jms.config.JmsListenerContainerFactory; +import org.springframework.jms.config.JmsListenerEndpointRegistrar; +import org.springframework.jms.config.SimpleJmsListenerEndpoint; +import org.springframework.jms.listener.adapter.ListenerExecutionFailedException; +import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; + +/** + * + * @author Stephane Nicoll + */ +public class AnnotationDrivenNamespaceTests extends AbstractJmsAnnotationDrivenTests { + + @Override + @Test + public void sampleConfiguration() { + ApplicationContext context = new ClassPathXmlApplicationContext( + "annotation-driven-sample-config.xml", getClass()); + testSampleConfiguration(context); + } + + @Override + @Test + public void fullConfiguration() { + ApplicationContext context = new ClassPathXmlApplicationContext( + "annotation-driven-full-config.xml", getClass()); + testFullConfiguration(context); + } + + @Override + @Test + public void customConfiguration() { + ApplicationContext context = new ClassPathXmlApplicationContext( + "annotation-driven-custom-registry.xml", getClass()); + testCustomConfiguration(context); + } + + @Override + @Test + public void defaultContainerFactoryConfiguration() { + ApplicationContext context = new ClassPathXmlApplicationContext( + "annotation-driven-custom-container-factory.xml", getClass()); + testDefaultContainerFactoryConfiguration(context); + } + + @Override + public void jmsHandlerMethodFactoryConfiguration() throws JMSException { + ApplicationContext context = new ClassPathXmlApplicationContext( + "annotation-driven-custom-handler-method-factory.xml", getClass()); + + thrown.expect(ListenerExecutionFailedException.class); + thrown.expectCause(Is.isA(MethodArgumentNotValidException.class)); + testJmsHandlerMethodFactoryConfiguration(context); + } + + static class CustomJmsListenerConfigurer implements JmsListenerConfigurer { + + private MessageListener messageListener; + + private JmsListenerContainerFactory containerFactory; + + @Override + public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setId("myCustomEndpointId"); + endpoint.setDestination("myQueue"); + endpoint.setMessageListener(messageListener); + registrar.registerEndpoint(endpoint, containerFactory); + } + + public void setMessageListener(MessageListener messageListener) { + this.messageListener = messageListener; + } + + public void setContainerFactory(JmsListenerContainerFactory containerFactory) { + this.containerFactory = containerFactory; + } + } +} diff --git a/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java b/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java new file mode 100644 index 0000000000..4e2cfe393e --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/annotation/EnableJmsTests.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.annotation; + +import javax.jms.JMSException; +import javax.jms.MessageListener; + +import org.hamcrest.core.Is; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.jms.config.DefaultJmsHandlerMethodFactory; +import org.springframework.jms.config.JmsHandlerMethodFactory; +import org.springframework.jms.config.JmsListenerContainerTestFactory; +import org.springframework.jms.config.JmsListenerEndpointRegistrar; +import org.springframework.jms.config.JmsListenerEndpointRegistry; +import org.springframework.jms.config.SimpleJmsListenerEndpoint; +import org.springframework.jms.listener.adapter.ListenerExecutionFailedException; +import org.springframework.jms.listener.adapter.MessageListenerAdapter; +import org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException; + +/** + * + * @author Stephane Nicoll + */ +public class EnableJmsTests extends AbstractJmsAnnotationDrivenTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Override + @Test + public void sampleConfiguration() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + EnableJmsConfig.class, SampleBean.class); + testSampleConfiguration(context); + } + + @Override + @Test + public void fullConfiguration() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + EnableJmsConfig.class, FullBean.class); + testFullConfiguration(context); + } + + @Override + @Test + public void customConfiguration() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + EnableJmsCustomConfig.class, CustomBean.class); + testCustomConfiguration(context); + } + + @Override + @Test + public void defaultContainerFactoryConfiguration() { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + EnableJmsDefaultContainerFactoryConfig.class, DefaultBean.class); + testDefaultContainerFactoryConfiguration(context); + } + + @Override + @Test + public void jmsHandlerMethodFactoryConfiguration() throws JMSException { + ConfigurableApplicationContext context = new AnnotationConfigApplicationContext( + EnableJmsHandlerMethodFactoryConfig.class, ValidationBean.class); + + thrown.expect(ListenerExecutionFailedException.class); + thrown.expectCause(Is.isA(MethodArgumentNotValidException.class)); + testJmsHandlerMethodFactoryConfiguration(context); + } + + @Test + public void unknownFactory() { + thrown.expect(BeanCreationException.class); + thrown.expectMessage("customFactory"); // Not found + new AnnotationConfigApplicationContext( + EnableJmsConfig.class, CustomBean.class); + } + + @EnableJms + @Configuration + static class EnableJmsConfig { + + @Bean + public JmsListenerContainerTestFactory defaultFactory() { + return new JmsListenerContainerTestFactory(); + } + + @Bean + public JmsListenerContainerTestFactory simpleFactory() { + return new JmsListenerContainerTestFactory(); + } + } + + @Configuration + @Import(EnableJmsConfig.class) + static class EnableJmsCustomConfig implements JmsListenerConfigurer { + + @Autowired + private EnableJmsConfig jmsConfig; + + @Override + public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { + registrar.setEndpointRegistry(customRegistry()); + + // Also register a custom endpoint + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setId("myCustomEndpointId"); + endpoint.setDestination("myQueue"); + endpoint.setMessageListener(simpleMessageListener()); + registrar.registerEndpoint(endpoint, jmsConfig.defaultFactory()); + } + + @Bean + public JmsListenerEndpointRegistry customRegistry() { + return new JmsListenerEndpointRegistry(); + } + + @Bean + public JmsListenerContainerTestFactory customFactory() { + return new JmsListenerContainerTestFactory(); + } + + @Bean + public MessageListener simpleMessageListener() { + return new MessageListenerAdapter(); + } + } + + @Configuration + @Import(EnableJmsConfig.class) + static class EnableJmsDefaultContainerFactoryConfig implements JmsListenerConfigurer { + + @Autowired + private EnableJmsConfig jmsConfig; + + @Override + public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { + registrar.setDefaultContainerFactory(jmsConfig.defaultFactory()); + } + } + + @Configuration + @Import(EnableJmsConfig.class) + static class EnableJmsHandlerMethodFactoryConfig implements JmsListenerConfigurer { + + @Override + public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { + registrar.setJmsHandlerMethodFactory(jmsHandlerMethodFactory()); + } + + @Bean + public JmsHandlerMethodFactory jmsHandlerMethodFactory() { + DefaultJmsHandlerMethodFactory factory = new DefaultJmsHandlerMethodFactory(); + factory.setValidator(new TestValidator()); + return factory; + } + } + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessorTests.java b/spring-jms/src/test/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessorTests.java new file mode 100644 index 0000000000..6b0a96c9ae --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessorTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.annotation; + +import static org.junit.Assert.*; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.Test; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jms.config.AbstractJmsListenerEndpoint; +import org.springframework.jms.config.JmsListenerContainerTestFactory; +import org.springframework.jms.config.JmsListenerEndpoint; +import org.springframework.jms.config.JmsListenerEndpointRegistry; +import org.springframework.jms.config.MessageListenerTestContainer; +import org.springframework.jms.config.MethodJmsListenerEndpoint; +import org.springframework.jms.listener.SimpleMessageListenerContainer; +import org.springframework.stereotype.Component; + +/** + * + * @author Stephane Nicoll + */ +public class JmsListenerAnnotationBeanPostProcessorTests { + + @Test + public void simpleMessageListener() { + final ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class, + SimpleMessageListenerTestBean.class); + + JmsListenerContainerTestFactory factory = context.getBean(JmsListenerContainerTestFactory.class); + assertEquals("one container should have been registered", 1, factory.getContainers().size()); + MessageListenerTestContainer container = factory.getContainers().get(0); + + JmsListenerEndpoint endpoint = container.getEndpoint(); + assertEquals("Wrong endpoint type", MethodJmsListenerEndpoint.class, endpoint.getClass()); + MethodJmsListenerEndpoint methodEndpoint = (MethodJmsListenerEndpoint) endpoint; + assertNotNull(methodEndpoint.getBean()); + assertNotNull(methodEndpoint.getMethod()); + assertNull(methodEndpoint.getResponseDestination()); + assertTrue(methodEndpoint.isQueue()); + assertTrue("Should have been started " + container, container.isStarted()); + + SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(); + methodEndpoint.setupMessageContainer(listenerContainer); + assertNotNull(listenerContainer.getMessageListener()); + + context.close(); // Close and stop the listeners + assertTrue("Should have been stopped " + container, container.isStopped()); + } + + @Test + public void metaAnnotationIsDiscovered() { + final ConfigurableApplicationContext context = new AnnotationConfigApplicationContext(Config.class, + MetaAnnotationTestBean.class); + + JmsListenerContainerTestFactory factory = context.getBean(JmsListenerContainerTestFactory.class); + assertEquals("one container should have been registered", 1, factory.getContainers().size()); + JmsListenerEndpoint endpoint = factory.getContainers().get(0).getEndpoint(); + assertEquals("metaTestQueue", ((AbstractJmsListenerEndpoint) endpoint).getDestination()); + } + + @Component + static class SimpleMessageListenerTestBean { + + @JmsListener(destination = "testQueue") + public void handleIt(String body) { + } + + } + + @Component + static class MetaAnnotationTestBean { + + @FooListener + public void handleIt(String body) { + } + } + + + @JmsListener(destination = "metaTestQueue") + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + static @interface FooListener { + + } + + @Configuration + static class Config { + + @Bean + public JmsListenerAnnotationBeanPostProcessor postProcessor() { + JmsListenerAnnotationBeanPostProcessor postProcessor = new JmsListenerAnnotationBeanPostProcessor(); + postProcessor.setEndpointRegistry(jmsListenerEndpointRegistry()); + postProcessor.setDefaultContainerFactory(testFactory()); + return postProcessor; + } + + @Bean + public JmsListenerEndpointRegistry jmsListenerEndpointRegistry() { + return new JmsListenerEndpointRegistry(); + } + + @Bean + public JmsListenerContainerTestFactory testFactory() { + return new JmsListenerContainerTestFactory(); + } + + } +} diff --git a/spring-jms/src/test/java/org/springframework/jms/config/DefaultJmsHandlerMethodFactoryTests.java b/spring-jms/src/test/java/org/springframework/jms/config/DefaultJmsHandlerMethodFactoryTests.java new file mode 100644 index 0000000000..0e9cd9cdcb --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/config/DefaultJmsHandlerMethodFactoryTests.java @@ -0,0 +1,203 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TestName; + +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.MethodParameter; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.messaging.Message; +import org.springframework.messaging.converter.ByteArrayMessageConverter; +import org.springframework.messaging.converter.MessageConversionException; +import org.springframework.messaging.converter.MessageConverter; +import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.ReflectionUtils; + +/** + * + * @author Stephane Nicoll + */ +public class DefaultJmsHandlerMethodFactoryTests { + + @Rule + public final TestName name = new TestName(); + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final SampleBean sample = new SampleBean(); + + @Test + public void customConversion() throws Exception { + DefaultJmsHandlerMethodFactory instance = createInstance(); + GenericConversionService conversionService = new GenericConversionService(); + conversionService.addConverter(SampleBean.class, String.class, new Converter() { + @Override + public String convert(SampleBean source) { + return "foo bar"; + } + }); + instance.setConversionService(conversionService); + instance.afterPropertiesSet(); + + InvocableHandlerMethod invocableHandlerMethod = + createInvocableHandlerMethod(instance, "simpleString", String.class); + + invocableHandlerMethod.invoke(MessageBuilder.withPayload(sample).build()); + assertMethodInvocation(sample, "simpleString"); + } + + @Test + public void customConversionServiceFailure() throws Exception { + DefaultJmsHandlerMethodFactory instance = createInstance(); + GenericConversionService conversionService = new GenericConversionService(); + assertFalse("conversion service should fail to convert payload", + conversionService.canConvert(Integer.class, String.class)); + instance.setConversionService(conversionService); + instance.afterPropertiesSet(); + + InvocableHandlerMethod invocableHandlerMethod = + createInvocableHandlerMethod(instance, "simpleString", String.class); + + thrown.expect(MessageConversionException.class); + invocableHandlerMethod.invoke(MessageBuilder.withPayload(123).build()); + } + + @Test + public void customMessageConverterFailure() throws Exception { + DefaultJmsHandlerMethodFactory instance = createInstance(); + MessageConverter messageConverter = new ByteArrayMessageConverter(); + instance.setMessageConverter(messageConverter); + instance.afterPropertiesSet(); + + InvocableHandlerMethod invocableHandlerMethod = + createInvocableHandlerMethod(instance, "simpleString", String.class); + + thrown.expect(MessageConversionException.class); + invocableHandlerMethod.invoke(MessageBuilder.withPayload(123).build()); + } + + @Test + public void customArgumentResolver() throws Exception { + DefaultJmsHandlerMethodFactory instance = createInstance(); + List customResolvers = new ArrayList(); + customResolvers.add(new CustomHandlerMethodArgumentResolver()); + instance.setCustomArgumentResolvers(customResolvers); + instance.afterPropertiesSet(); + + InvocableHandlerMethod invocableHandlerMethod = + createInvocableHandlerMethod(instance, "customArgumentResolver", Locale.class); + + invocableHandlerMethod.invoke(MessageBuilder.withPayload(123).build()); + assertMethodInvocation(sample, "customArgumentResolver"); + } + + @Test + public void overrideArgumentResolvers() throws Exception { + DefaultJmsHandlerMethodFactory instance = createInstance(); + List customResolvers = new ArrayList(); + customResolvers.add(new CustomHandlerMethodArgumentResolver()); + instance.setArgumentResolvers(customResolvers); + instance.afterPropertiesSet(); + + Message message = MessageBuilder.withPayload("sample").build(); + + // This will work as the local resolver is set + InvocableHandlerMethod invocableHandlerMethod = + createInvocableHandlerMethod(instance, "customArgumentResolver", Locale.class); + invocableHandlerMethod.invoke(message); + assertMethodInvocation(sample, "customArgumentResolver"); + + // This won't work as no resolver is known for the payload + InvocableHandlerMethod invocableHandlerMethod2 = + createInvocableHandlerMethod(instance, "simpleString", String.class); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage("No suitable resolver for"); + invocableHandlerMethod2.invoke(message); + } + + + private void assertMethodInvocation(SampleBean bean, String methodName) { + assertTrue("Method " + methodName + " should have been invoked", bean.invocations.get(methodName)); + } + + private InvocableHandlerMethod createInvocableHandlerMethod( + DefaultJmsHandlerMethodFactory factory, String methodName, Class... parameterTypes) { + return factory.createInvocableHandlerMethod(sample, getListenerMethod(methodName, parameterTypes)); + } + + private DefaultJmsHandlerMethodFactory createInstance() { + DefaultJmsHandlerMethodFactory factory = new DefaultJmsHandlerMethodFactory(); + factory.setApplicationContext(new StaticApplicationContext()); + return factory; + } + + private Method getListenerMethod(String methodName, Class... parameterTypes) { + Method method = ReflectionUtils.findMethod(SampleBean.class, + methodName, parameterTypes); + assertNotNull("no method found with name " + methodName + + " and parameters " + Arrays.toString(parameterTypes)); + return method; + } + + + static class SampleBean { + + private final Map invocations = new HashMap(); + + public void simpleString(String value) { + invocations.put("simpleString", true); + } + + public void customArgumentResolver(Locale locale) { + invocations.put("customArgumentResolver", true); + assertEquals("Wrong value for locale", Locale.getDefault(), locale); + } + } + + static class CustomHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterType().isAssignableFrom(Locale.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + return Locale.getDefault(); + } + } + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerFactoryIntegrationTests.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerFactoryIntegrationTests.java new file mode 100644 index 0000000000..cbcc4a904a --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerFactoryIntegrationTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.jms.Session; +import javax.jms.TextMessage; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.jms.StubTextMessage; +import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.jms.listener.SessionAwareMessageListener; +import org.springframework.jms.support.converter.MessageConversionException; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.util.ReflectionUtils; + +/** + * + * @author Stephane Nicoll + */ +public class JmsListenerContainerFactoryIntegrationTests { + + private final DefaultJmsListenerContainerFactory containerFactory = new DefaultJmsListenerContainerFactory(); + + private final DefaultJmsHandlerMethodFactory factory = new DefaultJmsHandlerMethodFactory(); + + private final JmsEndpointSampleBean sample = new JmsEndpointSampleBean(); + + @Before + public void setup() { + initializeFactory(factory); + } + + @Test + public void messageConverterUsedIfSet() throws JMSException { + containerFactory.setMessageConverter(new UpperCaseMessageConverter()); + + MethodJmsListenerEndpoint endpoint = createDefaultMethodJmsEndpoint("expectFooBarUpperCase", String.class); + Message message = new StubTextMessage("foo-bar"); + + invokeListener(endpoint, message); + assertListenerMethodInvocation("expectFooBarUpperCase"); + } + + static class JmsEndpointSampleBean { + + private final Map invocations = new HashMap(); + + public void expectFooBarUpperCase(@Payload String msg) { + invocations.put("expectFooBarUpperCase", true); + assertEquals("Unexpected payload message", "FOO-BAR", msg); + } + } + + @SuppressWarnings("unchecked") + private void invokeListener(JmsListenerEndpoint endpoint, Message message) throws JMSException { + DefaultMessageListenerContainer messageListenerContainer = + containerFactory.createMessageListenerContainer(endpoint); + Object listener = messageListenerContainer.getMessageListener(); + if (listener instanceof SessionAwareMessageListener) { + ((SessionAwareMessageListener) listener).onMessage(message, mock(Session.class)); + } + else { + ((MessageListener) listener).onMessage(message); + } + } + + private void assertListenerMethodInvocation(String methodName) { + assertTrue("Method " + methodName + " should have been invoked", sample.invocations.get(methodName)); + } + + + private MethodJmsListenerEndpoint createMethodJmsEndpoint( + DefaultJmsHandlerMethodFactory factory, Method method) { + MethodJmsListenerEndpoint endpoint = new MethodJmsListenerEndpoint(); + endpoint.setBean(sample); + endpoint.setMethod(method); + endpoint.setJmsHandlerMethodFactory(factory); + return endpoint; + } + + private MethodJmsListenerEndpoint createDefaultMethodJmsEndpoint(String methodName, Class... parameterTypes) { + return createMethodJmsEndpoint(this.factory, getListenerMethod(methodName, parameterTypes)); + } + + private Method getListenerMethod(String methodName, Class... parameterTypes) { + Method method = ReflectionUtils.findMethod(JmsEndpointSampleBean.class, + methodName, parameterTypes); + assertNotNull("no method found with name " + methodName + + " and parameters " + Arrays.toString(parameterTypes)); + return method; + } + + + private void initializeFactory(DefaultJmsHandlerMethodFactory factory) { + factory.setApplicationContext(new StaticApplicationContext()); + factory.afterPropertiesSet(); + } + + + private static class UpperCaseMessageConverter implements MessageConverter { + @Override + public Message toMessage(Object object, Session session) throws JMSException, MessageConversionException { + return new StubTextMessage(object.toString().toUpperCase()); + } + + @Override + public Object fromMessage(Message message) throws JMSException, MessageConversionException { + String content = ((TextMessage) message).getText(); + return content.toUpperCase(); + } + } +} + + diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerFactoryTests.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerFactoryTests.java new file mode 100644 index 0000000000..8f073200bc --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerFactoryTests.java @@ -0,0 +1,193 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import javax.jms.ConnectionFactory; +import javax.jms.MessageListener; +import javax.jms.Session; +import javax.transaction.TransactionManager; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.jms.StubConnectionFactory; +import org.springframework.jms.listener.AbstractMessageListenerContainer; +import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.jms.listener.SimpleMessageListenerContainer; +import org.springframework.jms.listener.adapter.MessageListenerAdapter; +import org.springframework.jms.listener.endpoint.JmsActivationSpecConfig; +import org.springframework.jms.listener.endpoint.JmsMessageEndpointManager; +import org.springframework.jms.listener.endpoint.StubJmsActivationSpecFactory; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.jms.support.converter.SimpleMessageConverter; +import org.springframework.jms.support.destination.DestinationResolver; +import org.springframework.jms.support.destination.DynamicDestinationResolver; + +/** + * + * @author Stephane Nicoll + */ +public class JmsListenerContainerFactoryTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final ConnectionFactory connectionFactory = new StubConnectionFactory(); + + private final DestinationResolver destinationResolver = new DynamicDestinationResolver(); + + private final MessageConverter messageConverter = new SimpleMessageConverter(); + + private final TransactionManager transactionManager = mock(TransactionManager.class); + + @Test + public void createSimpleContainer() { + SimpleJmsListenerContainerFactory factory = new SimpleJmsListenerContainerFactory(); + setDefaultJmsConfig(factory); + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + + MessageListener messageListener = new MessageListenerAdapter(); + endpoint.setMessageListener(messageListener); + endpoint.setDestination("myQueue"); + endpoint.setQueue(false); // See #setDefaultJmsConfig + + SimpleMessageListenerContainer container = factory.createMessageListenerContainer(endpoint); + + assertDefaultJmsConfig(container); + assertEquals(messageListener, container.getMessageListener()); + assertEquals("myQueue", container.getDestinationName()); + } + + + @Test + public void createJmsContainerFullConfig() { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + setDefaultJmsConfig(factory); + factory.setCacheLevel(DefaultMessageListenerContainer.CACHE_CONSUMER); + factory.setConcurrency("3-10"); + factory.setMaxMessagesPerTask(5); + + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + MessageListener messageListener = new MessageListenerAdapter(); + endpoint.setMessageListener(messageListener); + endpoint.setDestination("myQueue"); + endpoint.setQueue(false); // See #setDefaultJmsConfig + DefaultMessageListenerContainer container = factory.createMessageListenerContainer(endpoint); + + assertDefaultJmsConfig(container); + assertEquals(DefaultMessageListenerContainer.CACHE_CONSUMER, container.getCacheLevel()); + assertEquals(3, container.getConcurrentConsumers()); + assertEquals(10, container.getMaxConcurrentConsumers()); + assertEquals(5, container.getMaxMessagesPerTask()); + + assertEquals(messageListener, container.getMessageListener()); + assertEquals("myQueue", container.getDestinationName()); + } + + @Test + public void createJcaContainerFullConfig() { + DefaultJcaListenerContainerFactory factory = new DefaultJcaListenerContainerFactory(); + setDefaultJcaConfig(factory); + factory.getActivationSpecConfig().setConcurrency("10"); + + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + MessageListener messageListener = new MessageListenerAdapter(); + endpoint.setMessageListener(messageListener); + endpoint.setDestination("myQueue"); + endpoint.setQueue(false); // See #setDefaultJmsConfig + JmsMessageEndpointManager container = factory.createMessageListenerContainer(endpoint); + + assertDefaultJcaConfig(container); + assertEquals(10, container.getActivationSpecConfig().getMaxConcurrency()); + assertEquals(messageListener, container.getMessageListener()); + assertEquals("myQueue", container.getActivationSpecConfig().getDestinationName()); + } + + @Test + public void endpointCanOverrideConfig() { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setPubSubDomain(true); // topic + + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setMessageListener(new MessageListenerAdapter()); + endpoint.setQueue(true); // queue + + DefaultMessageListenerContainer container = factory.createMessageListenerContainer(endpoint); + assertEquals(false, container.isPubSubDomain()); // overridden by the endpoint config + } + + @Test + public void jcaExclusiveProperties() { + DefaultJcaListenerContainerFactory factory = new DefaultJcaListenerContainerFactory(); + factory.setDestinationResolver(destinationResolver); + factory.setActivationSpecFactory(new StubJmsActivationSpecFactory()); + + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setMessageListener(new MessageListenerAdapter()); + thrown.expect(IllegalStateException.class); + factory.createMessageListenerContainer(endpoint); + } + + private void setDefaultJmsConfig(AbstractJmsListenerContainerFactory factory) { + factory.setConnectionFactory(connectionFactory); + factory.setDestinationResolver(destinationResolver); + factory.setMessageConverter(messageConverter); + factory.setSessionTransacted(true); + factory.setSessionAcknowledgeMode(Session.DUPS_OK_ACKNOWLEDGE); + factory.setPubSubDomain(true); + factory.setSubscriptionDurable(true); + factory.setClientId("client-1234"); + } + + private void assertDefaultJmsConfig(AbstractMessageListenerContainer container) { + assertEquals(connectionFactory, container.getConnectionFactory()); + assertEquals(destinationResolver, container.getDestinationResolver()); + assertEquals(messageConverter, container.getMessageConverter()); + assertEquals(true, container.isSessionTransacted()); + assertEquals(Session.DUPS_OK_ACKNOWLEDGE, container.getSessionAcknowledgeMode()); + assertEquals(true, container.isPubSubDomain()); + assertEquals(true, container.isSubscriptionDurable()); + assertEquals("client-1234", container.getClientId()); + } + + private void setDefaultJcaConfig(DefaultJcaListenerContainerFactory factory) { + factory.setDestinationResolver(destinationResolver); + factory.setTransactionManager(transactionManager); + JmsActivationSpecConfig config = new JmsActivationSpecConfig(); + config.setMessageConverter(messageConverter); + config.setAcknowledgeMode(Session.DUPS_OK_ACKNOWLEDGE); + config.setPubSubDomain(true); + config.setSubscriptionDurable(true); + config.setClientId("client-1234"); + factory.setActivationSpecConfig(config); + } + + private void assertDefaultJcaConfig(JmsMessageEndpointManager container) { + assertEquals(messageConverter, container.getMessageConverter()); + JmsActivationSpecConfig config = container.getActivationSpecConfig(); + assertNotNull(config); + assertEquals(Session.DUPS_OK_ACKNOWLEDGE, config.getAcknowledgeMode()); + assertEquals(true, config.isPubSubDomain()); + assertEquals(true, config.isSubscriptionDurable()); + assertEquals("client-1234", config.getClientId()); + } + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java new file mode 100644 index 0000000000..9745a7c6ec --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerContainerTestFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Stephane Nicoll + */ +public class JmsListenerContainerTestFactory implements JmsListenerContainerFactory { + + private final List containers = + new ArrayList(); + + public List getContainers() { + return containers; + } + + @Override + public MessageListenerTestContainer createMessageListenerContainer(JmsListenerEndpoint endpoint) { + MessageListenerTestContainer container = new MessageListenerTestContainer(endpoint); + this.containers.add(container); + return container; + } + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointRegistrarTests.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointRegistrarTests.java new file mode 100644 index 0000000000..c811950315 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointRegistrarTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import static org.junit.Assert.*; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * + * @author Stephane Nicoll + */ +public class JmsListenerEndpointRegistrarTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final JmsListenerEndpointRegistrar registrar = new JmsListenerEndpointRegistrar(); + + private final JmsListenerEndpointRegistry registry = new JmsListenerEndpointRegistry(); + + private final JmsListenerContainerTestFactory containerFactory = new JmsListenerContainerTestFactory(); + + @Before + public void setup() { + registrar.setEndpointRegistry(registry); + } + + @Test + public void registerNullEndpoint() { + thrown.expect(IllegalArgumentException.class); + registrar.registerEndpoint(null, containerFactory); + } + + @Test + public void registerNullEndpointId() { + thrown.expect(IllegalArgumentException.class); + registrar.registerEndpoint(new SimpleJmsListenerEndpoint(), containerFactory); + } + + @Test + public void registerNullContainerFactoryIsAllowed() throws Exception { + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setId("some id"); + registrar.setDefaultContainerFactory(containerFactory); + registrar.registerEndpoint(endpoint, null); + registrar.afterPropertiesSet(); + assertNotNull("Container not created", registry.getContainer("some id")); + assertEquals(1, registry.getContainers().size()); + } + + @Test + public void registerNullContainerFactoryWithNoDefault() throws Exception { + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setId("some id"); + registrar.registerEndpoint(endpoint, null); + + thrown.expect(IllegalStateException.class); + thrown.expectMessage(endpoint.toString()); + registrar.afterPropertiesSet(); + } + + @Test + public void registerContainerWithoutFactory() throws Exception { + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setId("myEndpoint"); + registrar.setDefaultContainerFactory(containerFactory); + registrar.registerEndpoint(endpoint); + registrar.afterPropertiesSet(); + assertNotNull("Container not created", registry.getContainer("myEndpoint")); + assertEquals(1, registry.getContainers().size()); + } + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointRegistryTests.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointRegistryTests.java new file mode 100644 index 0000000000..0591e54c36 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointRegistryTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +/** + * + * @author Stephane Nicoll + */ +public class JmsListenerEndpointRegistryTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final JmsListenerEndpointRegistry registry = new JmsListenerEndpointRegistry(); + + private final JmsListenerContainerTestFactory containerFactory = new JmsListenerContainerTestFactory(); + + @Test + public void createWithNullEndpoint() { + thrown.expect(IllegalArgumentException.class); + registry.createJmsListenerContainer(null, containerFactory); + } + + @Test + public void createWithNullEndpointId() { + thrown.expect(IllegalArgumentException.class); + registry.createJmsListenerContainer(new SimpleJmsListenerEndpoint(), containerFactory); + } + + @Test + public void createWithNullContainerFactory() { + thrown.expect(IllegalArgumentException.class); + registry.createJmsListenerContainer(createEndpoint("foo", "myDestination"), null); + } + + @Test + public void createWithDuplicateEndpointId() { + registry.createJmsListenerContainer(createEndpoint("test", "queue"), containerFactory); + + thrown.expect(IllegalStateException.class); + registry.createJmsListenerContainer(createEndpoint("test", "queue"), containerFactory); + } + + private SimpleJmsListenerEndpoint createEndpoint(String id, String destinationName) { + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setId(id); + endpoint.setDestination(destinationName); + return endpoint; + } + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointTests.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointTests.java new file mode 100644 index 0000000000..66ee60878a --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsListenerEndpointTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +import javax.jms.MessageListener; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.jms.listener.MessageListenerContainer; +import org.springframework.jms.listener.adapter.MessageListenerAdapter; +import org.springframework.jms.listener.endpoint.JmsActivationSpecConfig; +import org.springframework.jms.listener.endpoint.JmsMessageEndpointManager; + +/** + * + * @author Stephane Nicoll + */ +public class JmsListenerEndpointTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void setupJmsMessageContainerFullConfig() { + DefaultMessageListenerContainer container = new DefaultMessageListenerContainer(); + MessageListener messageListener = new MessageListenerAdapter(); + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setDestination("myQueue"); + endpoint.setQueue(true); + endpoint.setSelector("foo = 'bar'"); + endpoint.setSubscription("mySubscription"); + endpoint.setMessageListener(messageListener); + + endpoint.setupMessageContainer(container); + assertEquals("myQueue", container.getDestinationName()); + assertFalse(container.isPubSubDomain()); + assertEquals("foo = 'bar'", container.getMessageSelector()); + assertEquals("mySubscription", container.getDurableSubscriptionName()); + assertEquals(messageListener, container.getMessageListener()); + } + + @Test + public void setupJcaMessageContainerFullConfig() { + JmsMessageEndpointManager container = new JmsMessageEndpointManager(); + MessageListener messageListener = new MessageListenerAdapter(); + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setDestination("myQueue"); + endpoint.setQueue(true); + endpoint.setSelector("foo = 'bar'"); + endpoint.setSubscription("mySubscription"); + endpoint.setMessageListener(messageListener); + + endpoint.setupMessageContainer(container); + JmsActivationSpecConfig config = container.getActivationSpecConfig(); + assertEquals("myQueue", config.getDestinationName()); + assertFalse(config.isPubSubDomain()); + assertEquals("foo = 'bar'", config.getMessageSelector()); + assertEquals("mySubscription", config.getDurableSubscriptionName()); + assertEquals(messageListener, container.getMessageListener()); + } + + + @Test + public void setupMessageContainerNoListener() { + DefaultMessageListenerContainer container = new DefaultMessageListenerContainer(); + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + + thrown.expect(IllegalStateException.class); + endpoint.setupMessageContainer(container); + } + + @Test + public void setupMessageContainerUnsupportedContainer() { + MessageListenerContainer container = mock(MessageListenerContainer.class); + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setMessageListener(new MessageListenerAdapter()); + + thrown.expect(IllegalArgumentException.class); + endpoint.setupMessageContainer(container); + } + + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java index 67719bae90..b17480c073 100644 --- a/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2014 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. @@ -16,6 +16,9 @@ package org.springframework.jms.config; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; + import java.util.HashSet; import java.util.Iterator; import java.util.Map; @@ -29,6 +32,7 @@ import javax.jms.TextMessage; import org.junit.After; import org.junit.Before; import org.junit.Test; + import org.springframework.beans.DirectFieldAccessor; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.ComponentDefinition; @@ -41,17 +45,16 @@ import org.springframework.context.Phased; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.jca.endpoint.GenericMessageEndpointManager; import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.jms.listener.adapter.MessageListenerAdapter; import org.springframework.jms.listener.endpoint.JmsMessageEndpointManager; import org.springframework.tests.sample.beans.TestBean; import org.springframework.util.ErrorHandler; -import static org.junit.Assert.*; -import static org.mockito.BDDMockito.*; - /** * @author Mark Fisher * @author Juergen Hoeller * @author Christian Dupuis + * @author Stephane Nicoll */ public class JmsNamespaceHandlerTests { @@ -80,6 +83,10 @@ public class JmsNamespaceHandlerTests { containers = context.getBeansOfType(GenericMessageEndpointManager.class); assertEquals("Context should contain 3 JCA endpoint containers", 3, containers.size()); + + Map containerFactories = + context.getBeansOfType(JmsListenerContainerFactory.class); + assertEquals("Context should contain 3 JmsListenerContainerFactory instances", 3, containerFactories.size()); } @Test @@ -99,8 +106,8 @@ public class JmsNamespaceHandlerTests { } else if (container.getConnectionFactory().equals(explicitConnectionFactory)) { explicitConnectionFactoryCount++; - assertEquals(1, container.getConcurrentConsumers()); - assertEquals(2, container.getMaxConcurrentConsumers()); + assertEquals(3, container.getConcurrentConsumers()); + assertEquals(5, container.getMaxConcurrentConsumers()); } } @@ -108,6 +115,72 @@ public class JmsNamespaceHandlerTests { assertEquals("2 containers should have the explicit connectionFactory", 2, explicitConnectionFactoryCount); } + @Test + public void testJcaContainerConfiguration() throws Exception { + Map containers = context.getBeansOfType(JmsMessageEndpointManager.class); + + assertTrue("listener3 not found", containers.containsKey("listener3")); + JmsMessageEndpointManager listener3 = containers.get("listener3"); + assertEquals("Wrong resource adapter", + context.getBean("testResourceAdapter"), listener3.getResourceAdapter()); + assertEquals("Wrong activation spec factory", context.getBean("testActivationSpecFactory"), + new DirectFieldAccessor(listener3).getPropertyValue("activationSpecFactory")); + + + Object endpointFactory = new DirectFieldAccessor(listener3).getPropertyValue("endpointFactory"); + Object messageListener = new DirectFieldAccessor(endpointFactory).getPropertyValue("messageListener"); + assertEquals("Wrong message listener", MessageListenerAdapter.class, messageListener.getClass()); + MessageListenerAdapter adapter = (MessageListenerAdapter) messageListener; + DirectFieldAccessor adapterFieldAccessor = new DirectFieldAccessor(adapter); + assertEquals("Message converter not set properly", context.getBean("testMessageConverter"), + adapterFieldAccessor.getPropertyValue("messageConverter")); + assertEquals("Wrong delegate", context.getBean("testBean1"), + adapterFieldAccessor.getPropertyValue("delegate")); + assertEquals("Wrong method name", "setName", + adapterFieldAccessor.getPropertyValue("defaultListenerMethod")); + } + + @Test + public void testJmsContainerFactoryConfiguration() { + Map containers = + context.getBeansOfType(DefaultJmsListenerContainerFactory.class); + DefaultJmsListenerContainerFactory factory = containers.get("testJmsFactory"); + assertNotNull("No factory registered with testJmsFactory id", factory); + + DefaultMessageListenerContainer container = + factory.createMessageListenerContainer(createDummyEndpoint()); + assertEquals("explicit connection factory not set", + context.getBean(EXPLICIT_CONNECTION_FACTORY), container.getConnectionFactory()); + assertEquals("explicit destination resolver not set", + context.getBean("testDestinationResolver"), container.getDestinationResolver()); + assertEquals("explicit message converter not set", + context.getBean("testMessageConverter"), container.getMessageConverter()); + assertEquals("wrong cache", DefaultMessageListenerContainer.CACHE_CONNECTION, container.getCacheLevel()); + assertEquals("wrong concurrency", 3, container.getConcurrentConsumers()); + assertEquals("wrong concurrency", 5, container.getMaxConcurrentConsumers()); + assertEquals("wrong prefetch", 50, container.getMaxMessagesPerTask()); + + assertEquals("phase cannot be customized by the factory", Integer.MAX_VALUE, container.getPhase()); + } + + @Test + public void testJcaContainerFactoryConfiguration() { + Map containers = + context.getBeansOfType(DefaultJcaListenerContainerFactory.class); + DefaultJcaListenerContainerFactory factory = containers.get("testJcaFactory"); + assertNotNull("No factory registered with testJcaFactory id", factory); + + JmsMessageEndpointManager container = + factory.createMessageListenerContainer(createDummyEndpoint()); + assertEquals("explicit resource adapter not set", + context.getBean("testResourceAdapter"),container.getResourceAdapter()); + assertEquals("explicit message converter not set", + context.getBean("testMessageConverter"), container.getActivationSpecConfig().getMessageConverter()); + assertEquals("wrong concurrency", 5, container.getActivationSpecConfig().getMaxConcurrency()); + assertEquals("Wrong prefetch", 50, container.getActivationSpecConfig().getPrefetchSize()); + assertEquals("phase cannot be customized by the factory", Integer.MAX_VALUE, container.getPhase()); + } + @Test public void testListeners() throws Exception { TestBean testBean1 = context.getBean("testBean1", TestBean.class); @@ -177,13 +250,24 @@ public class JmsNamespaceHandlerTests { @Test public void testComponentRegistration() { - assertTrue("Parser should have registered a component named 'listener1'", context.containsComponentDefinition("listener1")); - assertTrue("Parser should have registered a component named 'listener2'", context.containsComponentDefinition("listener2")); - assertTrue("Parser should have registered a component named 'listener3'", context.containsComponentDefinition("listener3")); - assertTrue("Parser should have registered a component named '" + DefaultMessageListenerContainer.class.getName() + "#0'", - context.containsComponentDefinition(DefaultMessageListenerContainer.class.getName() + "#0")); - assertTrue("Parser should have registered a component named '" + JmsMessageEndpointManager.class.getName() + "#0'", - context.containsComponentDefinition(JmsMessageEndpointManager.class.getName() + "#0")); + assertTrue("Parser should have registered a component named 'listener1'", + context.containsComponentDefinition("listener1")); + assertTrue("Parser should have registered a component named 'listener2'", + context.containsComponentDefinition("listener2")); + assertTrue("Parser should have registered a component named 'listener3'", + context.containsComponentDefinition("listener3")); + assertTrue("Parser should have registered a component named '" + + DefaultMessageListenerContainer.class.getName() + "#0'", + context.containsComponentDefinition(DefaultMessageListenerContainer.class.getName() + "#0")); + assertTrue("Parser should have registered a component named '" + + JmsMessageEndpointManager.class.getName() + "#0'", + context.containsComponentDefinition(JmsMessageEndpointManager.class.getName() + "#0")); + assertTrue("Parser should have registered a component named 'testJmsFactory", + context.containsComponentDefinition("testJmsFactory")); + assertTrue("Parser should have registered a component named 'testJcaFactory", + context.containsComponentDefinition("testJcaFactory")); + assertTrue("Parser should have registered a component named 'testJcaFactory", + context.containsComponentDefinition("onlyJmsFactory")); } @Test @@ -191,7 +275,7 @@ public class JmsNamespaceHandlerTests { Iterator iterator = context.getRegisteredComponents(); while (iterator.hasNext()) { ComponentDefinition compDef = (ComponentDefinition) iterator.next(); - assertNotNull("CompositeComponentDefinition '" + compDef.getName()+ "' has no source attachment", compDef.getSource()); + assertNotNull("CompositeComponentDefinition '" + compDef.getName() + "' has no source attachment", compDef.getSource()); validateComponentDefinition(compDef); } } @@ -228,6 +312,13 @@ public class JmsNamespaceHandlerTests { return ((Phased) container).getPhase(); } + private JmsListenerEndpoint createDummyEndpoint() { + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setMessageListener(new MessageListenerAdapter()); + endpoint.setDestination("testQueue"); + return endpoint; + } + public static class TestMessageListener implements MessageListener { diff --git a/spring-jms/src/test/java/org/springframework/jms/config/MessageListenerTestContainer.java b/spring-jms/src/test/java/org/springframework/jms/config/MessageListenerTestContainer.java new file mode 100644 index 0000000000..ddfd7a7441 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/config/MessageListenerTestContainer.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jms.JmsException; +import org.springframework.jms.listener.MessageListenerContainer; +import org.springframework.jms.support.converter.MessageConverter; + +/** + * + * @author Stephane Nicoll + */ +public class MessageListenerTestContainer + implements MessageListenerContainer, InitializingBean, DisposableBean { + + private final JmsListenerEndpoint endpoint; + + private boolean startInvoked; + + private boolean initializationInvoked; + + private boolean stopInvoked; + + private boolean destroyInvoked; + + MessageListenerTestContainer(JmsListenerEndpoint endpoint) { + this.endpoint = endpoint; + } + + public JmsListenerEndpoint getEndpoint() { + return endpoint; + } + + public boolean isStarted() { + return startInvoked && initializationInvoked; + } + + public boolean isStopped() { + return stopInvoked && destroyInvoked; + } + + @Override + public void start() throws JmsException { + if (startInvoked) { + throw new IllegalStateException("Start already invoked on " + this); + } + startInvoked = true; + } + + @Override + public boolean isRunning() { + return startInvoked && !stopInvoked; + } + + @Override + public void stop() throws JmsException { + if (stopInvoked) { + throw new IllegalStateException("Stop already invoked on " + this); + } + stopInvoked = true; + } + + @Override + public void setupMessageListener(Object messageListener) { + + } + + @Override + public MessageConverter getMessageConverter() { + return null; + } + + @Override + public void afterPropertiesSet() { + if (!startInvoked) { + throw new IllegalStateException("Start should have been invoked before " + + "afterPropertiesSet on " + this); + } + initializationInvoked = true; + } + + @Override + public void destroy() { + if (!stopInvoked) { + throw new IllegalStateException("Stop should have been invoked before " + + "destroy on " + this); + } + destroyInvoked = true; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("TestContainer{"); + sb.append("endpoint=").append(endpoint); + sb.append(", startInvoked=").append(startInvoked); + sb.append(", initializationInvoked=").append(initializationInvoked); + sb.append(", stopInvoked=").append(stopInvoked); + sb.append(", destroyInvoked=").append(destroyInvoked); + sb.append('}'); + return sb.toString(); + } +} diff --git a/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java b/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java new file mode 100644 index 0000000000..43bff74e8e --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java @@ -0,0 +1,418 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.*; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.ObjectMessage; +import javax.jms.QueueSender; +import javax.jms.Session; +import javax.jms.TextMessage; + +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.rules.TestName; + +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.jms.StubTextMessage; +import org.springframework.jms.listener.DefaultMessageListenerContainer; +import org.springframework.jms.listener.SimpleMessageListenerContainer; +import org.springframework.jms.listener.adapter.ListenerExecutionFailedException; +import org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter; +import org.springframework.jms.support.JmsMessageHeaderAccessor; +import org.springframework.jms.support.converter.JmsHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.converter.MessageConversionException; +import org.springframework.messaging.handler.annotation.Header; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.messaging.handler.annotation.Payload; +import org.springframework.messaging.handler.annotation.support.MethodArgumentTypeMismatchException; +import org.springframework.util.ReflectionUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; +import org.springframework.validation.annotation.Validated; + +/** + * + * @author Stephane Nicoll + */ +public class MethodJmsListenerEndpointTests { + + @Rule + public final TestName name = new TestName(); + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final DefaultJmsHandlerMethodFactory factory = new DefaultJmsHandlerMethodFactory(); + + private final DefaultMessageListenerContainer container = new DefaultMessageListenerContainer(); + + private final JmsEndpointSampleBean sample = new JmsEndpointSampleBean(); + + @Before + public void setup() { + initializeFactory(factory); + } + + @Test + public void createMessageListenerNoFactory() { + MethodJmsListenerEndpoint endpoint = new MethodJmsListenerEndpoint(); + endpoint.setBean(this); + endpoint.setMethod(getTestMethod()); + + thrown.expect(IllegalStateException.class); + endpoint.createMessageListener(container); + } + + @Test + public void createMessageListener() { + MethodJmsListenerEndpoint endpoint = new MethodJmsListenerEndpoint(); + endpoint.setBean(this); + endpoint.setMethod(getTestMethod()); + endpoint.setJmsHandlerMethodFactory(factory); + endpoint.setResponseDestination("myResponseQueue"); + + assertNotNull(endpoint.createMessageListener(container)); + } + + @Test + public void resolveMessageAndSession() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(javax.jms.Message.class, Session.class); + + Session session = mock(Session.class); + listener.onMessage(createSimpleJmsTextMessage("test"), session); + assertDefaultListenerMethodInvocation(); + } + + @Test + public void resolveGenericMessage() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(Message.class); + + Session session = mock(Session.class); + listener.onMessage(createSimpleJmsTextMessage("test"), session); + assertDefaultListenerMethodInvocation(); + } + + @Test + public void resolveHeaderAndPayload() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(String.class, int.class); + + Session session = mock(Session.class); + StubTextMessage message = createSimpleJmsTextMessage("my payload"); + message.setIntProperty("myCounter", 55); + listener.onMessage(message, session); + assertDefaultListenerMethodInvocation(); + } + + @Test + public void resolveHeaders() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(String.class, Map.class); + + Session session = mock(Session.class); + StubTextMessage message = createSimpleJmsTextMessage("my payload"); + message.setIntProperty("customInt", 1234); + message.setJMSMessageID("abcd-1234"); + listener.onMessage(message, session); + assertDefaultListenerMethodInvocation(); + } + + @Test + public void resolveMessageHeaders() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(MessageHeaders.class); + + Session session = mock(Session.class); + StubTextMessage message = createSimpleJmsTextMessage("my payload"); + message.setLongProperty("customLong", 4567L); + message.setJMSType("myMessageType"); + listener.onMessage(message, session); + assertDefaultListenerMethodInvocation(); + } + + @Test + public void resolveJmsMessageHeaderAccessor() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(JmsMessageHeaderAccessor.class); + + Session session = mock(Session.class); + StubTextMessage message = createSimpleJmsTextMessage("my payload"); + message.setBooleanProperty("customBoolean", true); + message.setJMSPriority(9); + listener.onMessage(message, session); + assertDefaultListenerMethodInvocation(); + } + + @Test + public void resolveObjectPayload() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(MyBean.class); + MyBean myBean = new MyBean(); + myBean.name = "myBean name"; + + Session session = mock(Session.class); + ObjectMessage message = mock(ObjectMessage.class); + given(message.getObject()).willReturn(myBean); + + listener.onMessage(message, session); + assertDefaultListenerMethodInvocation(); + } + + @Test + public void resolveConvertedPayload() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(Integer.class); + + Session session = mock(Session.class); + + listener.onMessage(createSimpleJmsTextMessage("33"), session); + assertDefaultListenerMethodInvocation(); + } + + @Test + public void processAndReply() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(String.class); + String body = "echo text"; + String correlationId = "link-1234"; + Destination replyDestination = new Destination() {}; + + TextMessage reply = mock(TextMessage.class); + QueueSender queueSender = mock(QueueSender.class); + Session session = mock(Session.class); + given(session.createTextMessage(body)).willReturn(reply); + given(session.createProducer(replyDestination)).willReturn(queueSender); + + listener.setDefaultResponseDestination(replyDestination); + StubTextMessage inputMessage = createSimpleJmsTextMessage(body); + inputMessage.setJMSCorrelationID(correlationId); + listener.onMessage(inputMessage, session); + assertDefaultListenerMethodInvocation(); + + verify(reply).setJMSCorrelationID(correlationId); + verify(queueSender).send(reply); + verify(queueSender).close(); + } + + @Test + public void validatePayloadValid() throws JMSException { + String methodName = "validatePayload"; + + DefaultJmsHandlerMethodFactory customFactory = new DefaultJmsHandlerMethodFactory(); + customFactory.setValidator(testValidator("invalid value")); + initializeFactory(customFactory); + + Method method = getListenerMethod(methodName, String.class); + MessagingMessageListenerAdapter listener = createInstance(customFactory, method); + Session session = mock(Session.class); + listener.onMessage(createSimpleJmsTextMessage("test"), session); // test is a valid value + assertListenerMethodInvocation(sample, methodName); + } + + @Test + public void validatePayloadInvalid() throws JMSException { + DefaultJmsHandlerMethodFactory customFactory = new DefaultJmsHandlerMethodFactory(); + customFactory.setValidator(testValidator("invalid value")); + + Method method = getListenerMethod("validatePayload", String.class); + MessagingMessageListenerAdapter listener = createInstance(customFactory, method); + Session session = mock(Session.class); + + thrown.expect(ListenerExecutionFailedException.class); + listener.onMessage(createSimpleJmsTextMessage("invalid value"), session); // test is an invalid value + + } + + // failure scenario + + @Test + public void invalidPayloadType() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(Integer.class); + Session session = mock(Session.class); + + thrown.expect(ListenerExecutionFailedException.class); + thrown.expectCause(Matchers.isA(MessageConversionException.class)); + thrown.expectMessage(getDefaultListenerMethod(Integer.class).toGenericString()); // ref to method + listener.onMessage(createSimpleJmsTextMessage("test"), session); // test is not a valid integer + } + + @Test + public void invalidMessagePayloadType() throws JMSException { + MessagingMessageListenerAdapter listener = createDefaultInstance(Message.class); + Session session = mock(Session.class); + + thrown.expect(ListenerExecutionFailedException.class); + thrown.expectCause(Matchers.isA(MethodArgumentTypeMismatchException.class)); + listener.onMessage(createSimpleJmsTextMessage("test"), session); // Message as Message + } + + private MessagingMessageListenerAdapter createInstance( + DefaultJmsHandlerMethodFactory factory, Method method) { + MethodJmsListenerEndpoint endpoint = new MethodJmsListenerEndpoint(); + endpoint.setBean(sample); + endpoint.setMethod(method); + endpoint.setJmsHandlerMethodFactory(factory); + return endpoint.createMessageListener(new SimpleMessageListenerContainer()); + } + + private MessagingMessageListenerAdapter createDefaultInstance(Class... parameterTypes) { + return createInstance(this.factory, getDefaultListenerMethod(parameterTypes)); + } + + private StubTextMessage createSimpleJmsTextMessage(String body) { + return new StubTextMessage(body); + } + + private Method getListenerMethod(String methodName, Class... parameterTypes) { + Method method = ReflectionUtils.findMethod(JmsEndpointSampleBean.class, + methodName, parameterTypes); + assertNotNull("no method found with name " + methodName + + " and parameters " + Arrays.toString(parameterTypes)); + return method; + } + + private Method getDefaultListenerMethod(Class... parameterTypes) { + return getListenerMethod(name.getMethodName(), parameterTypes); + } + + private void assertDefaultListenerMethodInvocation() { + assertListenerMethodInvocation(sample, name.getMethodName()); + } + + private void assertListenerMethodInvocation(JmsEndpointSampleBean bean, String methodName) { + assertTrue("Method " + methodName + " should have been invoked", bean.invocations.get(methodName)); + } + + private void initializeFactory(DefaultJmsHandlerMethodFactory factory) { + factory.setApplicationContext(new StaticApplicationContext()); + factory.afterPropertiesSet(); + } + + private Validator testValidator(final String invalidValue) { + + return new Validator() { + @Override + public boolean supports(Class clazz) { + return String.class.isAssignableFrom(clazz); + } + @Override + public void validate(Object target, Errors errors) { + String value = (String) target; + if (invalidValue.equals(value)) { + errors.reject("not a valid value"); + } + } + }; + } + + private Method getTestMethod() { + return ReflectionUtils.findMethod(MethodJmsListenerEndpointTests.class, name.getMethodName()); + } + + + static class JmsEndpointSampleBean { + + private final Map invocations = new HashMap(); + + public void resolveMessageAndSession(javax.jms.Message message, Session session) { + invocations.put("resolveMessageAndSession", true); + assertNotNull("Message not injected", message); + assertNotNull("Session not injected", session); + } + + public void resolveGenericMessage(Message message) { + invocations.put("resolveGenericMessage", true); + assertNotNull("Generic message not injected", message); + assertEquals("Wrong message payload", "test", message.getPayload()); + } + + public void resolveHeaderAndPayload(@Payload String content, @Header("myCounter") int counter) { + invocations.put("resolveHeaderAndPayload", true); + assertEquals("Wrong @Payload resolution", "my payload", content); + assertEquals("Wrong @Header resolution", 55, counter); + } + + public void resolveHeaders(String content, @Headers Map headers) { + invocations.put("resolveHeaders", true); + assertEquals("Wrong payload resolution", "my payload", content); + assertNotNull("headers not injected", headers); + assertEquals("Missing JMS message id header", "abcd-1234", headers.get(JmsHeaders.MESSAGE_ID)); + assertEquals("Missing custom header", 1234, headers.get("customInt")); + } + + public void resolveMessageHeaders(MessageHeaders headers) { + invocations.put("resolveMessageHeaders", true); + assertNotNull("MessageHeaders not injected", headers); + assertEquals("Missing JMS message type header", "myMessageType", headers.get(JmsHeaders.TYPE)); + assertEquals("Missing custom header", 4567L, (long) headers.get("customLong"), 0.0); + } + + public void resolveJmsMessageHeaderAccessor(JmsMessageHeaderAccessor headers) { + invocations.put("resolveJmsMessageHeaderAccessor", true); + assertNotNull("MessageHeaders not injected", headers); + assertEquals("Missing JMS message priority header", Integer.valueOf(9), headers.getPriority()); + assertEquals("Missing custom header", true, headers.getHeader("customBoolean")); + } + + public void resolveObjectPayload(MyBean bean) { + invocations.put("resolveObjectPayload", true); + assertNotNull("Object payload not injected", bean); + assertEquals("Wrong content for payload", "myBean name", bean.name); + } + + public void resolveConvertedPayload(Integer counter) { + invocations.put("resolveConvertedPayload", true); + assertNotNull("Payload not injected", counter); + assertEquals("Wrong content for payload", Integer.valueOf(33), counter); + } + + public String processAndReply(@Payload String content) { + invocations.put("processAndReply", true); + return content; + } + + public void validatePayload(@Validated String payload) { + invocations.put("validatePayload", true); + } + + public void invalidPayloadType(@Payload Integer payload) { + throw new IllegalStateException("Should never be called."); + } + + public void invalidMessagePayloadType(Message message) { + throw new IllegalStateException("Should never be called."); + } + + } + + @SuppressWarnings("serial") + static class MyBean implements Serializable { + private String name; + + } + + +} diff --git a/spring-jms/src/test/java/org/springframework/jms/config/SimpleJmsListenerEndpointTests.java b/spring-jms/src/test/java/org/springframework/jms/config/SimpleJmsListenerEndpointTests.java new file mode 100644 index 0000000000..34793c30f8 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/config/SimpleJmsListenerEndpointTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.config; + +import static org.junit.Assert.*; + +import javax.jms.MessageListener; + +import org.junit.Test; + +import org.springframework.jms.listener.SimpleMessageListenerContainer; +import org.springframework.jms.listener.adapter.MessageListenerAdapter; + +/** + * + * @author Stephane Nicoll + */ +public class SimpleJmsListenerEndpointTests { + + private final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); + + @Test + public void createListener() { + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + MessageListener messageListener = new MessageListenerAdapter(); + endpoint.setMessageListener(messageListener); + assertSame(messageListener, endpoint.createMessageListener(container)); + } +} diff --git a/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java new file mode 100644 index 0000000000..0a70da6fe9 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapterTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.listener.adapter; + +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.lang.reflect.Method; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Session; +import javax.jms.TextMessage; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.jms.StubTextMessage; +import org.springframework.jms.config.DefaultJmsHandlerMethodFactory; +import org.springframework.jms.support.converter.JmsHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.ReflectionUtils; + +/** + * + * @author Stephane Nicoll + */ +public class MessagingMessageListenerAdapterTests { + + private final DefaultJmsHandlerMethodFactory factory = new DefaultJmsHandlerMethodFactory(); + + private final SampleBean sample = new SampleBean(); + + @Before + public void setup() { + initializeFactory(factory); + } + + @Test + public void buildMessageWithStandardMessage() throws JMSException { + Destination replyTo = new Destination() {}; + Message result = MessageBuilder.withPayload("Response") + .setHeader("foo", "bar") + .setHeader(JmsHeaders.TYPE, "msg_type") + .setHeader(JmsHeaders.REPLY_TO, replyTo) + .build(); + + Session session = mock(Session.class); + given(session.createTextMessage("Response")).willReturn(new StubTextMessage("Response")); + javax.jms.Message replyMessage = getSimpleInstance().buildMessage(session, result); + + verify(session).createTextMessage("Response"); + assertNotNull("reply should never be null", replyMessage); + assertEquals("Response", ((TextMessage) replyMessage).getText()); + assertEquals("custom header not copied", "bar", replyMessage.getStringProperty("foo")); + assertEquals("type header not copied", "msg_type", replyMessage.getJMSType()); + assertEquals("replyTo header not copied", replyTo, replyMessage.getJMSReplyTo()); + } + + protected MessagingMessageListenerAdapter getSimpleInstance() { + Method m = ReflectionUtils.findMethod(SampleBean.class, "echo", Message.class); + return createInstance(m); + } + + protected MessagingMessageListenerAdapter createInstance(Method m) { + MessagingMessageListenerAdapter adapter = new MessagingMessageListenerAdapter(); + adapter.setHandlerMethod(factory.createInvocableHandlerMethod(sample, m)); + return adapter; + } + + private void initializeFactory(DefaultJmsHandlerMethodFactory factory) { + factory.setApplicationContext(new StaticApplicationContext()); + factory.afterPropertiesSet(); + } + + + private static class SampleBean { + + public Message echo(Message input) { + return MessageBuilder.withPayload(input.getPayload()) + .setHeader(JmsHeaders.TYPE, "reply") + .build(); + } + } +} diff --git a/spring-jms/src/test/java/org/springframework/jms/support/JmsMessageHeaderAccessorTests.java b/spring-jms/src/test/java/org/springframework/jms/support/JmsMessageHeaderAccessorTests.java new file mode 100644 index 0000000000..c89056d77b --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/support/JmsMessageHeaderAccessorTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.support; + +import static org.junit.Assert.*; + +import java.util.Map; + +import javax.jms.Destination; +import javax.jms.JMSException; + +import org.junit.Test; + +import org.springframework.jms.StubTextMessage; +import org.springframework.jms.support.converter.SimpleJmsHeaderMapper; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +/** + * + * @author Stephane Nicoll + */ +public class JmsMessageHeaderAccessorTests { + + @Test + public void validateJmsHeaders() throws JMSException { + Destination destination = new Destination() {}; + Destination replyTo = new Destination() {}; + + StubTextMessage jmsMessage = new StubTextMessage("test"); + + jmsMessage.setJMSCorrelationID("correlation-1234"); + jmsMessage.setJMSPriority(9); + jmsMessage.setJMSDestination(destination); + jmsMessage.setJMSDeliveryMode(1); + jmsMessage.setJMSExpiration(1234L); + jmsMessage.setJMSMessageID("abcd-1234"); + jmsMessage.setJMSPriority(9); + jmsMessage.setJMSReplyTo(replyTo); + jmsMessage.setJMSRedelivered(true); + jmsMessage.setJMSType("type"); + jmsMessage.setJMSTimestamp(4567L); + + Map mappedHeaders = new SimpleJmsHeaderMapper().toHeaders(jmsMessage); + Message message = MessageBuilder.withPayload("test").copyHeaders(mappedHeaders).build(); + JmsMessageHeaderAccessor headerAccessor = JmsMessageHeaderAccessor.wrap(message); + assertEquals("correlation-1234", headerAccessor.getCorrelationId()); + assertEquals(destination, headerAccessor.getDestination()); + assertEquals(Integer.valueOf(1), headerAccessor.getDeliveryMode()); + assertEquals(1234L, headerAccessor.getExpiration(), 0.0); + assertEquals("abcd-1234", headerAccessor.getMessageId()); + assertEquals(Integer.valueOf(9), headerAccessor.getPriority()); + assertEquals(replyTo, headerAccessor.getReplyTo()); + assertEquals(replyTo, headerAccessor.getReplyChannel()); + assertEquals(true, headerAccessor.getRedelivered()); + assertEquals("type", headerAccessor.getType()); + assertEquals(4567L, headerAccessor.getTimestamp(), 0.0); + } +} diff --git a/spring-jms/src/test/java/org/springframework/jms/support/converter/SimpleJmsHeaderMapperTests.java b/spring-jms/src/test/java/org/springframework/jms/support/converter/SimpleJmsHeaderMapperTests.java new file mode 100644 index 0000000000..ac19f73a49 --- /dev/null +++ b/spring-jms/src/test/java/org/springframework/jms/support/converter/SimpleJmsHeaderMapperTests.java @@ -0,0 +1,562 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.jms.support.converter; + +import static org.junit.Assert.*; + +import java.util.Date; +import java.util.Map; + +import javax.jms.DeliveryMode; +import javax.jms.Destination; +import javax.jms.JMSException; + +import org.junit.Test; + +import org.springframework.jms.StubTextMessage; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; + +/** + * + * @author Mark Fisher + * @author Gary Russel + * @author Stephane Nicoll + */ +public class SimpleJmsHeaderMapperTests { + + private final SimpleJmsHeaderMapper mapper = new SimpleJmsHeaderMapper(); + + // Outbound mapping + + @Test + public void jmsReplyToMappedFromHeader() throws JMSException { + Destination replyTo = new Destination() {}; + Message message = initBuilder() + .setHeader(JmsHeaders.REPLY_TO, replyTo).build(); + + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNotNull(jmsMessage.getJMSReplyTo()); + assertSame(replyTo, jmsMessage.getJMSReplyTo()); + } + + @Test + public void JmsReplyToIgnoredIfIncorrectType() throws JMSException { + Message message = initBuilder() + .setHeader(JmsHeaders.REPLY_TO, "not-a-destination").build(); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNull(jmsMessage.getJMSReplyTo()); + } + + @Test + public void jmsCorrelationIdMappedFromHeader() throws JMSException { + String jmsCorrelationId = "ABC-123"; + Message message = initBuilder() + .setHeader(JmsHeaders.CORRELATION_ID, jmsCorrelationId).build(); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNotNull(jmsMessage.getJMSCorrelationID()); + assertEquals(jmsCorrelationId, jmsMessage.getJMSCorrelationID()); + } + + @Test + public void jmsCorrelationIdNumberConvertsToString() throws JMSException { + Message message = initBuilder() + .setHeader(JmsHeaders.CORRELATION_ID, 123).build(); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertEquals("123", jmsMessage.getJMSCorrelationID()); + } + + @Test + public void jmsCorrelationIdIgnoredIfIncorrectType() throws JMSException { + Message message = initBuilder() + .setHeader(JmsHeaders.CORRELATION_ID, new Date()).build(); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNull(jmsMessage.getJMSCorrelationID()); + } + + @Test + public void jmsTypeMappedFromHeader() throws JMSException { + String jmsType = "testing"; + Message message = initBuilder() + .setHeader(JmsHeaders.TYPE, jmsType).build(); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNotNull(jmsMessage.getJMSType()); + assertEquals(jmsType, jmsMessage.getJMSType()); + } + + @Test + public void jmsTypeIgnoredIfIncorrectType() throws JMSException { + Message message = initBuilder() + .setHeader(JmsHeaders.TYPE, 123).build(); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNull(jmsMessage.getJMSType()); + } + + @Test + public void jmsReadOnlyPropertiesNotMapped() throws JMSException { + Message message = initBuilder() + .setHeader(JmsHeaders.DESTINATION, new Destination() {}) + .setHeader(JmsHeaders.DELIVERY_MODE, DeliveryMode.NON_PERSISTENT) + .setHeader(JmsHeaders.EXPIRATION, 1000L) + .setHeader(JmsHeaders.MESSAGE_ID, "abc-123") + .setHeader(JmsHeaders.PRIORITY, 9) + .setHeader(JmsHeaders.REDELIVERED, true) + .setHeader(JmsHeaders.TIMESTAMP, System.currentTimeMillis()) + .build(); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNull(jmsMessage.getJMSDestination()); + assertEquals(DeliveryMode.PERSISTENT, jmsMessage.getJMSDeliveryMode()); + assertEquals(0, jmsMessage.getJMSExpiration()); + assertNull(jmsMessage.getJMSMessageID()); + assertEquals(javax.jms.Message.DEFAULT_PRIORITY, jmsMessage.getJMSPriority()); + assertFalse(jmsMessage.getJMSRedelivered()); + assertEquals(0, jmsMessage.getJMSTimestamp()); + } + + @Test + public void contentTypePropertyMappedFromHeader() throws JMSException { + Message message = initBuilder() + .setHeader(MessageHeaders.CONTENT_TYPE, "foo") + .build(); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + Object value = jmsMessage.getObjectProperty(JmsHeaderMapper.CONTENT_TYPE_PROPERTY); + assertNotNull(value); + assertEquals("foo", value); + } + + @Test + public void userDefinedPropertyMappedFromHeader() throws JMSException { + Message message = initBuilder() + .setHeader("foo", 123) + .build(); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + Object value = jmsMessage.getObjectProperty("foo"); + assertNotNull(value); + assertEquals(Integer.class, value.getClass()); + assertEquals(123, ((Integer) value).intValue()); + } + + @Test + public void userDefinedPropertyMappedFromHeaderWithCustomPrefix() throws JMSException { + Message message = initBuilder() + .setHeader("foo", 123) + .build(); + mapper.setOutboundPrefix("custom_"); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + Object value = jmsMessage.getObjectProperty("custom_foo"); + assertNotNull(value); + assertEquals(Integer.class, value.getClass()); + assertEquals(123, ((Integer) value).intValue()); + } + + @Test + public void userDefinedPropertyWithUnsupportedType() throws JMSException { + Destination destination = new Destination() {}; + Message message = initBuilder() + .setHeader("destination", destination) + .build(); + javax.jms.Message jmsMessage = new StubTextMessage(); + mapper.fromHeaders(message.getHeaders(), jmsMessage); + Object value = jmsMessage.getObjectProperty("destination"); + assertNull(value); + } + + @Test + public void attemptToReadDisallowedCorrelationIdPropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public String getJMSCorrelationID() throws JMSException { + throw new JMSException("illegal property"); + } + }; + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, JmsHeaders.CORRELATION_ID); + } + + @Test + public void attemptToReadDisallowedDestinationPropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public Destination getJMSDestination() throws JMSException { + throw new JMSException("illegal property"); + } + }; + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, JmsHeaders.DESTINATION); + } + + @Test + public void attemptToReadDisallowedDeliveryModePropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public int getJMSDeliveryMode() throws JMSException { + throw new JMSException("illegal property"); + } + }; + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, JmsHeaders.DELIVERY_MODE); + } + + @Test + public void attemptToReadDisallowedExpirationPropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public long getJMSExpiration() throws JMSException { + throw new JMSException("illegal property"); + } + }; + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, JmsHeaders.EXPIRATION); + } + + @Test + public void attemptToReadDisallowedMessageIdPropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public String getJMSMessageID() throws JMSException { + throw new JMSException("illegal property"); + } + }; + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, JmsHeaders.MESSAGE_ID); + } + + @Test + public void attemptToReadDisallowedPriorityPropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public int getJMSPriority() throws JMSException { + throw new JMSException("illegal property"); + } + }; + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, JmsHeaders.PRIORITY); + } + + @Test + public void attemptToReadDisallowedReplyToPropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public Destination getJMSReplyTo() throws JMSException { + throw new JMSException("illegal property"); + } + }; + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, JmsHeaders.REPLY_TO); + } + + @Test + public void attemptToReadDisallowedRedeliveredPropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public boolean getJMSRedelivered() throws JMSException { + throw new JMSException("illegal property"); + } + }; + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, JmsHeaders.REDELIVERED); + } + + @Test + public void attemptToReadDisallowedTypePropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public String getJMSType() throws JMSException { + throw new JMSException("illegal property"); + } + }; + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, JmsHeaders.TYPE); + } + + @Test + public void attemptToReadDisallowedTimestampPropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public long getJMSTimestamp() throws JMSException { + throw new JMSException("illegal property"); + } + }; + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, JmsHeaders.TIMESTAMP); + } + + @Test + public void attemptToReadDisallowedUserPropertyIsNotFatal() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public Object getObjectProperty(String name) throws JMSException { + if (name.equals("fail")) { + throw new JMSException("illegal property"); + } + else { + return super.getObjectProperty(name); + } + } + }; + jmsMessage.setBooleanProperty("fail", true); + assertAttemptReadDisallowedPropertyIsNotFatal(jmsMessage, "fail"); + } + + + // Inbound mapping + + @Test + public void jmsCorrelationIdMappedToHeader() throws JMSException { + String correlationId = "ABC-123"; + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setJMSCorrelationID(correlationId); + assertInboundHeader(jmsMessage, JmsHeaders.CORRELATION_ID, correlationId); + } + + @Test + public void destinationMappedToHeader() throws JMSException { + Destination destination = new Destination() {}; + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setJMSDestination(destination); + assertInboundHeader(jmsMessage, JmsHeaders.DESTINATION, destination); + } + + @Test + public void jmsDeliveryModeMappedToHeader() throws JMSException { + int deliveryMode = 1; + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setJMSDeliveryMode(deliveryMode); + assertInboundHeader(jmsMessage, JmsHeaders.DELIVERY_MODE, deliveryMode); + } + + @Test + public void jmsExpirationMappedToHeader() throws JMSException { + long expiration = 1000L; + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setJMSExpiration(expiration); + assertInboundHeader(jmsMessage, JmsHeaders.EXPIRATION, expiration); + } + + @Test + public void jmsMessageIdMappedToHeader() throws JMSException { + String messageId = "ID:ABC-123"; + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setJMSMessageID(messageId); + assertInboundHeader(jmsMessage, JmsHeaders.MESSAGE_ID, messageId); + } + + @Test + public void jmsPriorityMappedToHeader() throws JMSException { + int priority = 8; + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setJMSPriority(priority); + assertInboundHeader(jmsMessage, JmsHeaders.PRIORITY, priority); + } + + @Test + public void jmsReplyToMappedToHeader() throws JMSException { + Destination replyTo = new Destination() {}; + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setJMSReplyTo(replyTo); + assertInboundHeader(jmsMessage, JmsHeaders.REPLY_TO, replyTo); + } + + @Test + public void jmsTypeMappedToHeader() throws JMSException { + String type = "testing"; + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setJMSType(type); + assertInboundHeader(jmsMessage, JmsHeaders.TYPE, type); + } + + @Test + public void jmsTimestampMappedToHeader() throws JMSException { + long timestamp = 123L; + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setJMSTimestamp(timestamp); + assertInboundHeader(jmsMessage, JmsHeaders.TIMESTAMP, timestamp); + } + + @Test + public void contentTypePropertyMappedToHeader() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setStringProperty("content_type", "foo"); + assertInboundHeader(jmsMessage, MessageHeaders.CONTENT_TYPE, "foo"); + } + + @Test + public void userDefinedPropertyMappedToHeader() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setIntProperty("foo", 123); + assertInboundHeader(jmsMessage, "foo", 123); + } + + @Test + public void userDefinedPropertyMappedToHeaderWithCustomPrefix() throws JMSException { + javax.jms.Message jmsMessage = new StubTextMessage(); + jmsMessage.setIntProperty("foo", 123); + mapper.setInboundPrefix("custom_"); + assertInboundHeader(jmsMessage, "custom_foo", 123); + } + + @Test + public void propertyMappingExceptionIsNotFatal() throws JMSException { + Message message = initBuilder() + .setHeader("foo", 123) + .setHeader("bad", 456) + .setHeader("bar", 789) + .build(); + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public void setObjectProperty(String name, Object value) throws JMSException { + if (name.equals("bad")) { + throw new JMSException("illegal property"); + } + super.setObjectProperty(name, value); + } + }; + mapper.fromHeaders(message.getHeaders(), jmsMessage); + Object foo = jmsMessage.getObjectProperty("foo"); + assertNotNull(foo); + Object bar = jmsMessage.getObjectProperty("bar"); + assertNotNull(bar); + Object bad = jmsMessage.getObjectProperty("bad"); + assertNull(bad); + } + + @Test + public void illegalArgumentExceptionIsNotFatal() throws JMSException { + Message message = initBuilder() + .setHeader("foo", 123) + .setHeader("bad", 456) + .setHeader("bar", 789) + .build(); + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public void setObjectProperty(String name, Object value) throws JMSException { + if (name.equals("bad")) { + throw new IllegalArgumentException("illegal property"); + } + super.setObjectProperty(name, value); + } + }; + mapper.fromHeaders(message.getHeaders(), jmsMessage); + Object foo = jmsMessage.getObjectProperty("foo"); + assertNotNull(foo); + Object bar = jmsMessage.getObjectProperty("bar"); + assertNotNull(bar); + Object bad = jmsMessage.getObjectProperty("bad"); + assertNull(bad); + } + + @Test + public void attemptToWriteDisallowedReplyToPropertyIsNotFatal() throws JMSException { + Message message = initBuilder() + .setHeader(JmsHeaders.REPLY_TO, new Destination() {}) + .setHeader("foo", "bar") + .build(); + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public void setJMSReplyTo(Destination replyTo) throws JMSException { + throw new JMSException("illegal property"); + } + }; + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNull(jmsMessage.getJMSReplyTo()); + assertNotNull(jmsMessage.getStringProperty("foo")); + assertEquals("bar", jmsMessage.getStringProperty("foo")); + } + + @Test + public void attemptToWriteDisallowedTypePropertyIsNotFatal() throws JMSException { + Message message = initBuilder() + .setHeader(JmsHeaders.TYPE, "someType") + .setHeader("foo", "bar") + .build(); + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public void setJMSType(String type) throws JMSException { + throw new JMSException("illegal property"); + } + }; + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNull(jmsMessage.getJMSType()); + assertNotNull(jmsMessage.getStringProperty("foo")); + assertEquals("bar", jmsMessage.getStringProperty("foo")); + } + + @Test + public void attemptToWriteDisallowedCorrelationIdStringPropertyIsNotFatal() throws JMSException { + Message message = initBuilder() + .setHeader(JmsHeaders.CORRELATION_ID, "abc") + .setHeader("foo", "bar") + .build(); + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public void setJMSCorrelationID(String correlationId) throws JMSException { + throw new JMSException("illegal property"); + } + }; + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNull(jmsMessage.getJMSCorrelationID()); + assertNotNull(jmsMessage.getStringProperty("foo")); + assertEquals("bar", jmsMessage.getStringProperty("foo")); + } + + @Test + public void attemptToWriteDisallowedCorrelationIdNumberPropertyIsNotFatal() throws JMSException { + Message message = initBuilder() + .setHeader(JmsHeaders.CORRELATION_ID, 123) + .setHeader("foo", "bar") + .build(); + javax.jms.Message jmsMessage = new StubTextMessage() { + @Override + public void setJMSCorrelationID(String correlationId) throws JMSException { + throw new JMSException("illegal property"); + } + }; + mapper.fromHeaders(message.getHeaders(), jmsMessage); + assertNull(jmsMessage.getJMSCorrelationID()); + assertNotNull(jmsMessage.getStringProperty("foo")); + assertEquals("bar", jmsMessage.getStringProperty("foo")); + } + + + private void assertInboundHeader(javax.jms.Message jmsMessage, String headerId, Object value) { + Map headers = mapper.toHeaders(jmsMessage); + Object headerValue = headers.get(headerId); + if (value == null) { + assertNull(headerValue); + } + else { + assertNotNull(headerValue); + assertEquals(value.getClass(), headerValue.getClass()); + assertEquals(value, headerValue); + } + } + + private void assertAttemptReadDisallowedPropertyIsNotFatal(javax.jms.Message jmsMessage, String headerId) + throws JMSException { + jmsMessage.setStringProperty("foo", "bar"); + Map headers = mapper.toHeaders(jmsMessage); + assertNull(headers.get(headerId)); + assertNotNull(headers.get("foo")); + assertEquals("bar", headers.get("foo")); + } + + private MessageBuilder initBuilder() { + return MessageBuilder.withPayload("test"); + } +} diff --git a/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-container-factory.xml b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-container-factory.xml new file mode 100644 index 0000000000..98edbfff80 --- /dev/null +++ b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-container-factory.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-handler-method-factory.xml b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-handler-method-factory.xml new file mode 100644 index 0000000000..a70ff5eaf5 --- /dev/null +++ b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-handler-method-factory.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-registry.xml b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-registry.xml new file mode 100644 index 0000000000..a05f762ae9 --- /dev/null +++ b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-custom-registry.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-full-config.xml b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-full-config.xml new file mode 100644 index 0000000000..99974d8868 --- /dev/null +++ b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-full-config.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-sample-config.xml b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-sample-config.xml new file mode 100644 index 0000000000..2bc2820432 --- /dev/null +++ b/spring-jms/src/test/resources/org/springframework/jms/annotation/annotation-driven-sample-config.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-jms/src/test/java/org/springframework/jms/config/jmsNamespaceHandlerTests.xml b/spring-jms/src/test/resources/org/springframework/jms/config/jmsNamespaceHandlerTests.xml similarity index 77% rename from spring-jms/src/test/java/org/springframework/jms/config/jmsNamespaceHandlerTests.xml rename to spring-jms/src/test/resources/org/springframework/jms/config/jmsNamespaceHandlerTests.xml index 3569172c02..4145be1828 100644 --- a/spring-jms/src/test/java/org/springframework/jms/config/jmsNamespaceHandlerTests.xml +++ b/spring-jms/src/test/resources/org/springframework/jms/config/jmsNamespaceHandlerTests.xml @@ -3,12 +3,13 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jms="http://www.springframework.org/schema/jms" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd - http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-4.0.xsd"> + http://www.springframework.org/schema/jms http://www.springframework.org/schema/jms/spring-jms-4.1.xsd"> - + cache="connection" concurrency="3-5" prefetch="50" receive-timeout="100" recovery-interval="1000" phase="99"> @@ -26,8 +27,9 @@ - + @@ -36,6 +38,13 @@ + + + diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/GenericMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/GenericMessageConverter.java new file mode 100644 index 0000000000..fcd38a2981 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/GenericMessageConverter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.messaging.converter; + +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConversionService; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; + +/** + * An extension of the {@link SimpleMessageConverter} that uses a + * {@link ConversionService} to convert the payload of the message + * to the requested type. + * + *

Return {@code null} if the conversion service cannot convert + * from the payload type to the requested type. + * + * @author Stephane Nicoll + * @since 4.1 + * @see ConversionService + */ +public class GenericMessageConverter extends SimpleMessageConverter { + + private final ConversionService conversionService; + + /** + * Create a new instance with the {@link ConversionService} to use. + */ + public GenericMessageConverter(ConversionService conversionService) { + Assert.notNull(conversionService, "ConversionService must not be null"); + this.conversionService = conversionService; + } + + @Override + public Object fromMessage(Message message, Class targetClass) { + Object payload = message.getPayload(); + if (conversionService.canConvert(payload.getClass(), targetClass)) { + try { + return conversionService.convert(payload, targetClass); + } + catch (ConversionException e) { + throw new MessageConversionException(message, "Failed to convert message payload '" + + payload + "' to '" + targetClass.getName() + "'", e); + } + } + return null; + } +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java index 7b6f5bb666..c011cbcfdf 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java @@ -32,7 +32,7 @@ import org.springframework.util.ReflectionUtils; * Invokes the handler method for a given message after resolving * its method argument values through registered {@link HandlerMethodArgumentResolver}s. * - *

Use {@link #setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite)} + *

Use {@link #setMessageMethodArgumentResolvers(HandlerMethodArgumentResolver)} * to customize the list of argument resolvers. * * @author Rossen Stoyanchev @@ -40,7 +40,7 @@ import org.springframework.util.ReflectionUtils; */ public class InvocableHandlerMethod extends HandlerMethod { - private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite(); + private HandlerMethodArgumentResolver argumentResolvers = new HandlerMethodArgumentResolverComposite(); private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); @@ -74,7 +74,7 @@ public class InvocableHandlerMethod extends HandlerMethod { /** * Set {@link HandlerMethodArgumentResolver}s to use to use for resolving method argument values. */ - public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) { + public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolver argumentResolvers) { this.argumentResolvers = argumentResolvers; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/mapping/HeaderMapper.java b/spring-messaging/src/main/java/org/springframework/messaging/mapping/HeaderMapper.java new file mode 100644 index 0000000000..12e24da185 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/mapping/HeaderMapper.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.messaging.mapping; + +import java.util.Map; + +import org.springframework.messaging.MessageHeaders; + +/** + * Generic strategy interface for mapping {@link MessageHeaders} to and from other + * types of objects. This would typically be used by adapters where the "other type" + * has a concept of headers or properties (HTTP, JMS, AMQP, etc). + * + * @author Mark Fisher + * @param type of the instance to and from which headers will be mapped. + */ +public interface HeaderMapper { + + void fromHeaders(MessageHeaders headers, T target); + + Map toHeaders(T source); + +} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/mapping/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/mapping/package-info.java new file mode 100644 index 0000000000..bbc1438c71 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/mapping/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2002-2014 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 + * + * 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. + */ + +/** + * Provides classes related to mapping to/from message headers. + */ +package org.springframework.messaging.mapping; \ No newline at end of file diff --git a/spring-messaging/src/test/java/org/springframework/messaging/converter/GenericMessageConverterTests.java b/spring-messaging/src/test/java/org/springframework/messaging/converter/GenericMessageConverterTests.java new file mode 100644 index 0000000000..0ce6af9f86 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/converter/GenericMessageConverterTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2014 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 + * + * 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.springframework.messaging.converter; + +import static org.hamcrest.Matchers.isA; +import static org.junit.Assert.*; + +import java.util.Locale; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.core.convert.ConversionException; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +/** + * + * @author Stephane Nicoll + */ +public class GenericMessageConverterTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private final ConversionService conversionService = new DefaultConversionService(); + private final GenericMessageConverter converter = new GenericMessageConverter(conversionService); + + @Test + public void fromMessageWithConversion() { + Message content = MessageBuilder.withPayload("33").build(); + assertEquals(33, converter.fromMessage(content, Integer.class)); + } + + @Test + public void fromMessageNoConverter() { + Message content = MessageBuilder.withPayload(1234).build(); + assertNull("No converter from integer to locale", converter.fromMessage(content, Locale.class)); + } + + @Test + public void fromMessageWithFailedConversion() { + Message content = MessageBuilder.withPayload("test not a number").build(); + thrown.expect(MessageConversionException.class); + thrown.expectCause(isA(ConversionException.class)); + converter.fromMessage(content, Integer.class); + } +}