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 + *

+ * + *

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); + } +}