From e6b7d6222af211803adc3d9e6ac181122192f6cb Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 28 Oct 2008 13:47:36 +0000 Subject: [PATCH] Initial import of the JMS module --- build-spring-framework/build.xml | 1 + org.springframework.jms/build.xml | 6 + org.springframework.jms/ivy.xml | 35 + org.springframework.jms/pom.xml | 45 + .../jms/IllegalStateException.java | 32 + .../jms/InvalidClientIDException.java | 32 + .../jms/InvalidDestinationException.java | 32 + .../jms/InvalidSelectorException.java | 32 + .../org/springframework/jms/JmsException.java | 94 ++ .../jms/JmsSecurityException.java | 32 + .../jms/MessageEOFException.java | 32 + .../jms/MessageFormatException.java | 32 + .../jms/MessageNotReadableException.java | 32 + .../jms/MessageNotWriteableException.java | 32 + .../jms/ResourceAllocationException.java | 32 + .../jms/TransactionInProgressException.java | 32 + .../jms/TransactionRolledBackException.java | 32 + .../jms/UncategorizedJmsException.java | 56 + .../AbstractListenerContainerParser.java | 288 +++++ .../config/JcaListenerContainerParser.java | 111 ++ .../config/JmsListenerContainerParser.java | 166 +++ .../jms/config/JmsNamespaceHandler.java | 36 + .../springframework/jms/config/package.html | 8 + .../jms/config/spring-jms-2.5.xsd | 439 +++++++ .../jms/connection/CachedMessageConsumer.java | 90 ++ .../jms/connection/CachedMessageProducer.java | 173 +++ .../connection/CachingConnectionFactory.java | 468 ++++++++ .../connection/ChainedExceptionListener.java | 64 + .../connection/ConnectionFactoryUtils.java | 417 +++++++ .../DelegatingConnectionFactory.java | 163 +++ .../jms/connection/JmsResourceHolder.java | 211 ++++ .../jms/connection/JmsTransactionManager.java | 321 +++++ .../connection/JmsTransactionManager102.java | 148 +++ .../jms/connection/SessionProxy.java | 41 + .../connection/SingleConnectionFactory.java | 587 +++++++++ .../SingleConnectionFactory102.java | 129 ++ .../connection/SmartConnectionFactory.java | 39 + ...ynchedLocalTransactionFailedException.java | 42 + ...ransactionAwareConnectionFactoryProxy.java | 329 ++++++ ...erCredentialsConnectionFactoryAdapter.java | 299 +++++ .../jms/connection/package.html | 8 + .../jms/core/BrowserCallback.java | 46 + .../jms/core/JmsOperations.java | 425 +++++++ .../springframework/jms/core/JmsTemplate.java | 1040 ++++++++++++++++ .../jms/core/JmsTemplate102.java | 255 ++++ .../jms/core/MessageCreator.java | 49 + .../jms/core/MessagePostProcessor.java | 46 + .../jms/core/ProducerCallback.java | 54 + .../jms/core/SessionCallback.java | 44 + .../org/springframework/jms/core/package.html | 8 + .../jms/core/support/JmsGatewaySupport.java | 118 ++ .../jms/core/support/package.html | 8 + .../AbstractJmsListeningContainer.java | 609 ++++++++++ .../AbstractMessageListenerContainer.java | 676 +++++++++++ ...stractPollingMessageListenerContainer.java | 514 ++++++++ .../DefaultMessageListenerContainer.java | 1044 +++++++++++++++++ .../DefaultMessageListenerContainer102.java | 115 ++ .../LocallyExposedJmsResourceHolder.java | 36 + .../listener/SessionAwareMessageListener.java | 56 + .../SimpleMessageListenerContainer.java | 346 ++++++ .../SimpleMessageListenerContainer102.java | 97 ++ .../listener/SubscriptionNameProvider.java | 38 + .../ListenerExecutionFailedException.java | 39 + .../adapter/MessageListenerAdapter.java | 656 +++++++++++ .../adapter/MessageListenerAdapter102.java | 98 ++ .../jms/listener/adapter/package.html | 9 + .../DefaultJmsActivationSpecFactory.java | 181 +++ .../endpoint/JmsActivationSpecConfig.java | 128 ++ .../endpoint/JmsActivationSpecFactory.java | 49 + .../endpoint/JmsMessageEndpointFactory.java | 126 ++ .../endpoint/JmsMessageEndpointManager.java | 142 +++ .../StandardJmsActivationSpecFactory.java | 204 ++++ .../jms/listener/endpoint/package.html | 7 + .../springframework/jms/listener/package.html | 9 + .../AbstractPoolingServerSessionFactory.java | 179 +++ .../CommonsPoolServerSessionFactory.java | 274 +++++ .../serversession/ListenerSessionManager.java | 58 + .../serversession/ServerSessionFactory.java | 63 + ...ServerSessionMessageListenerContainer.java | 256 ++++ ...verSessionMessageListenerContainer102.java | 101 ++ .../SimpleServerSessionFactory.java | 138 +++ .../jms/listener/serversession/package.html | 8 + .../java/org/springframework/jms/package.html | 8 + .../remoting/JmsInvokerClientInterceptor.java | 442 +++++++ .../remoting/JmsInvokerProxyFactoryBean.java | 92 ++ .../remoting/JmsInvokerServiceExporter.java | 191 +++ .../springframework/jms/remoting/package.html | 11 + .../jms/support/JmsAccessor.java | 212 ++++ .../springframework/jms/support/JmsUtils.java | 311 +++++ .../converter/MessageConversionException.java | 48 + .../support/converter/MessageConverter.java | 59 + .../converter/SimpleMessageConverter.java | 227 ++++ .../converter/SimpleMessageConverter102.java | 64 + .../jms/support/converter/package.html | 8 + .../BeanFactoryDestinationResolver.java | 84 ++ .../CachingDestinationResolver.java | 43 + .../DestinationResolutionException.java | 47 + .../destination/DestinationResolver.java | 57 + .../DynamicDestinationResolver.java | 109 ++ .../destination/JmsDestinationAccessor.java | 103 ++ .../destination/JndiDestinationResolver.java | 162 +++ .../jms/support/destination/package.html | 7 + .../springframework/jms/support/package.html | 8 + .../src/main/java/overview.html | 7 + .../src/test/resources/log4j.xml | 28 + org.springframework.jms/template.mf | 29 + 106 files changed, 15904 insertions(+) create mode 100644 org.springframework.jms/build.xml create mode 100644 org.springframework.jms/ivy.xml create mode 100644 org.springframework.jms/pom.xml create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/IllegalStateException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/InvalidClientIDException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/InvalidDestinationException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/InvalidSelectorException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/JmsException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/JmsSecurityException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/MessageEOFException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/MessageFormatException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/MessageNotReadableException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/MessageNotWriteableException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/ResourceAllocationException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/TransactionInProgressException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/TransactionRolledBackException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/UncategorizedJmsException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/config/AbstractListenerContainerParser.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/config/JcaListenerContainerParser.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/config/JmsNamespaceHandler.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/config/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/config/spring-jms-2.5.xsd create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/CachedMessageConsumer.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/ChainedExceptionListener.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/ConnectionFactoryUtils.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/DelegatingConnectionFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsResourceHolder.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager102.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/SessionProxy.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory102.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/SmartConnectionFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/SynchedLocalTransactionFailedException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/TransactionAwareConnectionFactoryProxy.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/connection/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/BrowserCallback.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/JmsOperations.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/JmsTemplate.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/JmsTemplate102.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/MessageCreator.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/MessagePostProcessor.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/ProducerCallback.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/SessionCallback.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/core/support/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer102.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/LocallyExposedJmsResourceHolder.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/SessionAwareMessageListener.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer102.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/SubscriptionNameProvider.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/ListenerExecutionFailedException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter102.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/DefaultJmsActivationSpecFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/AbstractPoolingServerSessionFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/CommonsPoolServerSessionFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ListenerSessionManager.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionMessageListenerContainer.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionMessageListenerContainer102.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/SimpleServerSessionFactory.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerClientInterceptor.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerProxyFactoryBean.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerServiceExporter.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/remoting/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/JmsAccessor.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/JmsUtils.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MessageConversionException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MessageConverter.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter102.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/converter/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/destination/BeanFactoryDestinationResolver.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/destination/CachingDestinationResolver.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DestinationResolutionException.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/destination/JndiDestinationResolver.java create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/destination/package.html create mode 100644 org.springframework.jms/src/main/java/org/springframework/jms/support/package.html create mode 100644 org.springframework.jms/src/main/java/overview.html create mode 100644 org.springframework.jms/src/test/resources/log4j.xml create mode 100644 org.springframework.jms/template.mf diff --git a/build-spring-framework/build.xml b/build-spring-framework/build.xml index 09bf924498a..1f595b74fb1 100644 --- a/build-spring-framework/build.xml +++ b/build-spring-framework/build.xml @@ -9,6 +9,7 @@ + diff --git a/org.springframework.jms/build.xml b/org.springframework.jms/build.xml new file mode 100644 index 00000000000..5df978288cd --- /dev/null +++ b/org.springframework.jms/build.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/org.springframework.jms/ivy.xml b/org.springframework.jms/ivy.xml new file mode 100644 index 00000000000..7b2a9b22126 --- /dev/null +++ b/org.springframework.jms/ivy.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.jms/pom.xml b/org.springframework.jms/pom.xml new file mode 100644 index 00000000000..09470aba0a7 --- /dev/null +++ b/org.springframework.jms/pom.xml @@ -0,0 +1,45 @@ + + + + org.springframework + org.springframework.parent + 3.0-M1-SNAPSHOT + + 4.0.0 + org.springframework.jdbc + jar + Spring Framework: JDBC + + + org.springframework + org.springframework.core + + + org.springframework + org.springframework.beans + + + org.springframework + org.springframework.context + + + org.springframework + org.springframework.transaction + + + javax.transaction + com.springsource.javax.transaction + true + + + com.mchange.c3p0 + com.springsource.com.mchange.v2.c3p0 + true + + + com.experlog.xapool + com.springsource.org.enhydra.jdbc + true + + + \ No newline at end of file diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/IllegalStateException.java b/org.springframework.jms/src/main/java/org/springframework/jms/IllegalStateException.java new file mode 100644 index 00000000000..20dde5d45ec --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/IllegalStateException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS IllegalStateException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.IllegalStateException + */ +public class IllegalStateException extends JmsException { + + public IllegalStateException(javax.jms.IllegalStateException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/InvalidClientIDException.java b/org.springframework.jms/src/main/java/org/springframework/jms/InvalidClientIDException.java new file mode 100644 index 00000000000..07607f7381f --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/InvalidClientIDException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS InvalidClientIDException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.InvalidClientIDException + */ +public class InvalidClientIDException extends JmsException { + + public InvalidClientIDException(javax.jms.InvalidClientIDException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/InvalidDestinationException.java b/org.springframework.jms/src/main/java/org/springframework/jms/InvalidDestinationException.java new file mode 100644 index 00000000000..6d794927ced --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/InvalidDestinationException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS InvalidDestinationException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.InvalidDestinationException + */ +public class InvalidDestinationException extends JmsException { + + public InvalidDestinationException(javax.jms.InvalidDestinationException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/InvalidSelectorException.java b/org.springframework.jms/src/main/java/org/springframework/jms/InvalidSelectorException.java new file mode 100644 index 00000000000..797dee10a39 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/InvalidSelectorException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS InvalidSelectorException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.InvalidSelectorException + */ +public class InvalidSelectorException extends JmsException { + + public InvalidSelectorException(javax.jms.InvalidSelectorException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/JmsException.java b/org.springframework.jms/src/main/java/org/springframework/jms/JmsException.java new file mode 100644 index 00000000000..5bebcebfdf4 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/JmsException.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2008 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 javax.jms.JMSException; + +import org.springframework.core.NestedRuntimeException; + +/** + * Base class for exception thrown by the framework whenever it + * encounters a problem related to JMS. + * + * @author Mark Pollack + * @author Juergen Hoeller + * @since 1.1 + */ +public abstract class JmsException extends NestedRuntimeException { + + /** + * Constructor that takes a message. + * @param msg the detail message + */ + public JmsException(String msg) { + super(msg); + } + + /** + * Constructor that takes a message and a root cause. + * @param msg the detail message + * @param cause the cause of the exception. This argument is generally + * expected to be a proper subclass of {@link javax.jms.JMSException}, + * but can also be a JNDI NamingException or the like. + */ + public JmsException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructor that takes a plain root cause, intended for + * subclasses mirroring corresponding javax.jms exceptions. + * @param cause the cause of the exception. This argument is generally + * expected to be a proper subclass of {@link javax.jms.JMSException}. + */ + public JmsException(Throwable cause) { + super(cause != null ? cause.getMessage() : null, cause); + } + + + /** + * Convenience method to get the vendor specific error code if + * the root cause was an instance of JMSException. + * @return a string specifying the vendor-specific error code if the + * root cause is an instance of JMSException, or null + */ + public String getErrorCode() { + Throwable cause = getCause(); + if (cause instanceof JMSException) { + return ((JMSException) cause).getErrorCode(); + } + return null; + } + + /** + * Return the detail message, including the message from the linked exception + * if there is one. + * @see javax.jms.JMSException#getLinkedException() + */ + public String getMessage() { + String message = super.getMessage(); + Throwable cause = getCause(); + if (cause instanceof JMSException) { + Exception linkedEx = ((JMSException) cause).getLinkedException(); + if (linkedEx != null && cause.getMessage().indexOf(linkedEx.getMessage()) == -1) { + message = message + "; nested exception is " + linkedEx; + } + } + return message; + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/JmsSecurityException.java b/org.springframework.jms/src/main/java/org/springframework/jms/JmsSecurityException.java new file mode 100644 index 00000000000..123a42d4e5f --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/JmsSecurityException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS JMSSecurityException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.JMSSecurityException + */ +public class JmsSecurityException extends JmsException { + + public JmsSecurityException(javax.jms.JMSSecurityException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/MessageEOFException.java b/org.springframework.jms/src/main/java/org/springframework/jms/MessageEOFException.java new file mode 100644 index 00000000000..cc3aaf1b5db --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/MessageEOFException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS MessageEOFException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.MessageEOFException + */ +public class MessageEOFException extends JmsException { + + public MessageEOFException(javax.jms.MessageEOFException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/MessageFormatException.java b/org.springframework.jms/src/main/java/org/springframework/jms/MessageFormatException.java new file mode 100644 index 00000000000..4366d5376e7 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/MessageFormatException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS MessageFormatException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.MessageFormatException + */ +public class MessageFormatException extends JmsException { + + public MessageFormatException(javax.jms.MessageFormatException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/MessageNotReadableException.java b/org.springframework.jms/src/main/java/org/springframework/jms/MessageNotReadableException.java new file mode 100644 index 00000000000..307e6adb490 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/MessageNotReadableException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS MessageNotReadableException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.MessageNotReadableException + */ +public class MessageNotReadableException extends JmsException { + + public MessageNotReadableException(javax.jms.MessageNotReadableException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/MessageNotWriteableException.java b/org.springframework.jms/src/main/java/org/springframework/jms/MessageNotWriteableException.java new file mode 100644 index 00000000000..6cf3b464db5 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/MessageNotWriteableException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS MessageNotWriteableException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.MessageNotWriteableException + */ +public class MessageNotWriteableException extends JmsException { + + public MessageNotWriteableException(javax.jms.MessageNotWriteableException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/ResourceAllocationException.java b/org.springframework.jms/src/main/java/org/springframework/jms/ResourceAllocationException.java new file mode 100644 index 00000000000..f143aa93f0d --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/ResourceAllocationException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS ResourceAllocationException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.ResourceAllocationException + */ +public class ResourceAllocationException extends JmsException { + + public ResourceAllocationException(javax.jms.ResourceAllocationException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/TransactionInProgressException.java b/org.springframework.jms/src/main/java/org/springframework/jms/TransactionInProgressException.java new file mode 100644 index 00000000000..77afc211532 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/TransactionInProgressException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS TransactionInProgressException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.TransactionInProgressException + */ +public class TransactionInProgressException extends JmsException { + + public TransactionInProgressException(javax.jms.TransactionInProgressException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/TransactionRolledBackException.java b/org.springframework.jms/src/main/java/org/springframework/jms/TransactionRolledBackException.java new file mode 100644 index 00000000000..e493d3d7548 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/TransactionRolledBackException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2007 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; + +/** + * Runtime exception mirroring the JMS TransactionRolledBackException. + * + * @author Mark Pollack + * @since 1.1 + * @see javax.jms.TransactionRolledBackException + */ +public class TransactionRolledBackException extends JmsException { + + public TransactionRolledBackException(javax.jms.TransactionRolledBackException cause) { + super(cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/UncategorizedJmsException.java b/org.springframework.jms/src/main/java/org/springframework/jms/UncategorizedJmsException.java new file mode 100644 index 00000000000..6dd076bb5a5 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/UncategorizedJmsException.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2007 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; + +/** + * JmsException to be thrown when no other matching subclass found. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public class UncategorizedJmsException extends JmsException { + + /** + * Constructor that takes a message. + * @param msg the detail message + */ + public UncategorizedJmsException(String msg) { + super(msg); + } + + /** + * Constructor that takes a message and a root cause. + * @param msg the detail message + * @param cause the cause of the exception. This argument is generally + * expected to be a proper subclass of {@link javax.jms.JMSException}, + * but can also be a JNDI NamingException or the like. + */ + public UncategorizedJmsException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Constructor that takes a root cause only. + * @param cause the cause of the exception. This argument is generally + * expected to be a proper subclass of {@link javax.jms.JMSException}, + * but can also be a JNDI NamingException or the like. + */ + public UncategorizedJmsException(Throwable cause) { + super("Uncategorized exception occured during JMS processing", cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/config/AbstractListenerContainerParser.java b/org.springframework.jms/src/main/java/org/springframework/jms/config/AbstractListenerContainerParser.java new file mode 100644 index 00000000000..211304950ef --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/config/AbstractListenerContainerParser.java @@ -0,0 +1,288 @@ +/* + * Copyright 2002-2007 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.Session; + +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.parsing.BeanComponentDefinition; +import org.springframework.beans.factory.parsing.CompositeComponentDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; + +/** + * Abstract parser for JMS listener container elements, providing support for + * common properties that are identical for all listener container variants. + * + * @author Juergen Hoeller + * @since 2.5 + */ +abstract class AbstractListenerContainerParser implements BeanDefinitionParser { + + protected static final String LISTENER_ELEMENT = "listener"; + + protected static final String ID_ATTRIBUTE = "id"; + + protected static final String DESTINATION_ATTRIBUTE = "destination"; + + protected static final String SUBSCRIPTION_ATTRIBUTE = "subscription"; + + protected static final String SELECTOR_ATTRIBUTE = "selector"; + + protected static final String REF_ATTRIBUTE = "ref"; + + protected static final String METHOD_ATTRIBUTE = "method"; + + protected static final String DESTINATION_RESOLVER_ATTRIBUTE = "destination-resolver"; + + protected static final String MESSAGE_CONVERTER_ATTRIBUTE = "message-converter"; + + protected static final String RESPONSE_DESTINATION_ATTRIBUTE = "response-destination"; + + protected static final String DESTINATION_TYPE_ATTRIBUTE = "destination-type"; + + protected static final String DESTINATION_TYPE_QUEUE = "queue"; + + protected static final String DESTINATION_TYPE_TOPIC = "topic"; + + protected static final String DESTINATION_TYPE_DURABLE_TOPIC = "durableTopic"; + + protected static final String CLIENT_ID_ATTRIBUTE = "client-id"; + + protected static final String ACKNOWLEDGE_ATTRIBUTE = "acknowledge"; + + protected static final String ACKNOWLEDGE_AUTO = "auto"; + + protected static final String ACKNOWLEDGE_CLIENT = "client"; + + protected static final String ACKNOWLEDGE_DUPS_OK = "dups-ok"; + + protected static final String ACKNOWLEDGE_TRANSACTED = "transacted"; + + protected static final String TRANSACTION_MANAGER_ATTRIBUTE = "transaction-manager"; + + protected static final String CONCURRENCY_ATTRIBUTE = "concurrency"; + + protected static final String PREFETCH_ATTRIBUTE = "prefetch"; + + + public BeanDefinition parse(Element element, ParserContext parserContext) { + CompositeComponentDefinition compositeDef = + new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element)); + parserContext.pushContainingComponent(compositeDef); + + 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 = child.getLocalName(); + if (LISTENER_ELEMENT.equals(localName)) { + parseListener((Element) child, element, parserContext); + } + } + } + + parserContext.popAndRegisterContainingComponent(); + return null; + } + + private void parseListener(Element listenerEle, Element containerEle, ParserContext parserContext) { + RootBeanDefinition listenerDef = new RootBeanDefinition(); + listenerDef.setSource(parserContext.extractSource(listenerEle)); + + String ref = listenerEle.getAttribute(REF_ATTRIBUTE); + if (!StringUtils.hasText(ref)) { + parserContext.getReaderContext().error( + "Listener 'ref' attribute contains empty value.", listenerEle); + } + listenerDef.getPropertyValues().addPropertyValue("delegate", new RuntimeBeanReference(ref)); + + String method = null; + if (listenerEle.hasAttribute(METHOD_ATTRIBUTE)) { + method = listenerEle.getAttribute(METHOD_ATTRIBUTE); + if (!StringUtils.hasText(method)) { + parserContext.getReaderContext().error( + "Listener 'method' attribute contains empty value.", listenerEle); + } + } + listenerDef.getPropertyValues().addPropertyValue("defaultListenerMethod", method); + + if (containerEle.hasAttribute(MESSAGE_CONVERTER_ATTRIBUTE)) { + String messageConverter = containerEle.getAttribute(MESSAGE_CONVERTER_ATTRIBUTE); + listenerDef.getPropertyValues().addPropertyValue("messageConverter", + new RuntimeBeanReference(messageConverter)); + } + + BeanDefinition containerDef = parseContainer(listenerEle, containerEle, parserContext); + + if (listenerEle.hasAttribute(RESPONSE_DESTINATION_ATTRIBUTE)) { + String responseDestination = listenerEle.getAttribute(RESPONSE_DESTINATION_ATTRIBUTE); + boolean pubSubDomain = indicatesPubSub(containerDef); + listenerDef.getPropertyValues().addPropertyValue( + pubSubDomain ? "defaultResponseTopicName" : "defaultResponseQueueName", responseDestination); + if (containerDef.getPropertyValues().contains("destinationResolver")) { + listenerDef.getPropertyValues().addPropertyValue("destinationResolver", + containerDef.getPropertyValues().getPropertyValue("destinationResolver").getValue()); + } + } + + // Remain JMS 1.0.2 compatible for the adapter if the container class indicates this. + boolean jms102 = indicatesJms102(containerDef); + listenerDef.setBeanClassName( + "org.springframework.jms.listener.adapter.MessageListenerAdapter" + (jms102 ? "102" : "")); + + containerDef.getPropertyValues().addPropertyValue("messageListener", listenerDef); + + String containerBeanName = listenerEle.getAttribute(ID_ATTRIBUTE); + // If no bean id is given auto generate one using the ReaderContext's BeanNameGenerator + if (!StringUtils.hasText(containerBeanName)) { + containerBeanName = parserContext.getReaderContext().generateBeanName(containerDef); + } + + // Register the listener and fire event + parserContext.registerBeanComponent(new BeanComponentDefinition(containerDef, containerBeanName)); + } + + protected abstract BeanDefinition parseContainer( + Element listenerEle, Element containerEle, ParserContext parserContext); + + protected boolean indicatesPubSub(BeanDefinition containerDef) { + return false; + } + + protected boolean indicatesJms102(BeanDefinition containerDef) { + return false; + } + + protected void parseListenerConfiguration(Element ele, ParserContext parserContext, BeanDefinition configDef) { + String destination = ele.getAttribute(DESTINATION_ATTRIBUTE); + if (!StringUtils.hasText(destination)) { + parserContext.getReaderContext().error( + "Listener 'destination' attribute contains empty value.", ele); + } + configDef.getPropertyValues().addPropertyValue("destinationName", destination); + + if (ele.hasAttribute(SUBSCRIPTION_ATTRIBUTE)) { + String subscription = ele.getAttribute(SUBSCRIPTION_ATTRIBUTE); + if (!StringUtils.hasText(subscription)) { + parserContext.getReaderContext().error( + "Listener 'subscription' attribute contains empty value.", ele); + } + configDef.getPropertyValues().addPropertyValue("durableSubscriptionName", subscription); + } + + if (ele.hasAttribute(SELECTOR_ATTRIBUTE)) { + String selector = ele.getAttribute(SELECTOR_ATTRIBUTE); + if (!StringUtils.hasText(selector)) { + parserContext.getReaderContext().error( + "Listener 'selector' attribute contains empty value.", ele); + } + configDef.getPropertyValues().addPropertyValue("messageSelector", selector); + } + } + + protected void parseContainerConfiguration(Element ele, ParserContext parserContext, BeanDefinition configDef) { + String destinationType = ele.getAttribute(DESTINATION_TYPE_ATTRIBUTE); + boolean pubSubDomain = false; + boolean subscriptionDurable = false; + if (DESTINATION_TYPE_DURABLE_TOPIC.equals(destinationType)) { + pubSubDomain = true; + subscriptionDurable = true; + } + else if (DESTINATION_TYPE_TOPIC.equals(destinationType)) { + pubSubDomain = true; + } + else if ("".equals(destinationType) || DESTINATION_TYPE_QUEUE.equals(destinationType)) { + // the default: queue + } + else { + parserContext.getReaderContext().error("Invalid listener container 'destination-type': " + + "only \"queue\", \"topic\" and \"durableTopic\" supported.", ele); + } + configDef.getPropertyValues().addPropertyValue("pubSubDomain", Boolean.valueOf(pubSubDomain)); + configDef.getPropertyValues().addPropertyValue("subscriptionDurable", Boolean.valueOf(subscriptionDurable)); + + if (ele.hasAttribute(CLIENT_ID_ATTRIBUTE)) { + String clientId = ele.getAttribute(CLIENT_ID_ATTRIBUTE); + if (!StringUtils.hasText(clientId)) { + parserContext.getReaderContext().error( + "Listener 'client-id' attribute contains empty value.", ele); + } + configDef.getPropertyValues().addPropertyValue("clientId", clientId); + } + } + + protected Integer parseAcknowledgeMode(Element ele, ParserContext parserContext) { + String acknowledge = ele.getAttribute(ACKNOWLEDGE_ATTRIBUTE); + if (StringUtils.hasText(acknowledge)) { + int acknowledgeMode = Session.AUTO_ACKNOWLEDGE; + if (ACKNOWLEDGE_TRANSACTED.equals(acknowledge)) { + acknowledgeMode = Session.SESSION_TRANSACTED; + } + else if (ACKNOWLEDGE_DUPS_OK.equals(acknowledge)) { + acknowledgeMode = Session.DUPS_OK_ACKNOWLEDGE; + } + else if (ACKNOWLEDGE_CLIENT.equals(acknowledge)) { + acknowledgeMode = Session.CLIENT_ACKNOWLEDGE; + } + else if (!ACKNOWLEDGE_AUTO.equals(acknowledge)) { + parserContext.getReaderContext().error("Invalid listener container 'acknowledge' setting [" + + acknowledge + "]: only \"auto\", \"client\", \"dups-ok\" and \"transacted\" supported.", ele); + } + return new Integer(acknowledgeMode); + } + else { + return null; + } + } + + protected boolean indicatesPubSubConfig(BeanDefinition configDef) { + return ((Boolean) configDef.getPropertyValues().getPropertyValue("pubSubDomain").getValue()).booleanValue(); + } + + protected int[] parseConcurrency(Element ele, ParserContext parserContext) { + String concurrency = ele.getAttribute(CONCURRENCY_ATTRIBUTE); + if (!StringUtils.hasText(concurrency)) { + return null; + } + try { + int separatorIndex = concurrency.indexOf('-'); + if (separatorIndex != -1) { + int[] result = new int[2]; + result[0] = Integer.parseInt(concurrency.substring(0, separatorIndex)); + result[1] = Integer.parseInt(concurrency.substring(separatorIndex + 1, concurrency.length())); + return result; + } + else { + return new int[] {1, Integer.parseInt(concurrency)}; + } + } + catch (NumberFormatException ex) { + parserContext.getReaderContext().error("Invalid concurrency value [" + concurrency + "]: only " + + "single maximum integer (e.g. \"5\") and minimum-maximum combo (e.g. \"3-5\") supported.", ele, ex); + return null; + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/config/JcaListenerContainerParser.java b/org.springframework.jms/src/main/java/org/springframework/jms/config/JcaListenerContainerParser.java new file mode 100644 index 00000000000..433ee94956d --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/config/JcaListenerContainerParser.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2007 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.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; + +/** + * Parser for the JMS <jca-listener-container> element. + * + * @author Juergen Hoeller + * @since 2.5 + */ +class JcaListenerContainerParser extends AbstractListenerContainerParser { + + private static final String RESOURCE_ADAPTER_ATTRIBUTE = "resource-adapter"; + + private static final String ACTIVATION_SPEC_FACTORY_ATTRIBUTE = "activation-spec-factory"; + + + protected BeanDefinition parseContainer(Element listenerEle, Element containerEle, ParserContext parserContext) { + RootBeanDefinition containerDef = new RootBeanDefinition(); + containerDef.setSource(parserContext.extractSource(containerEle)); + containerDef.setBeanClassName("org.springframework.jms.listener.endpoint.JmsMessageEndpointManager"); + + String resourceAdapterBeanName = "resourceAdapter"; + if (containerEle.hasAttribute(RESOURCE_ADAPTER_ATTRIBUTE)) { + resourceAdapterBeanName = containerEle.getAttribute(RESOURCE_ADAPTER_ATTRIBUTE); + if (!StringUtils.hasText(resourceAdapterBeanName)) { + parserContext.getReaderContext().error( + "Listener container 'resource-adapter' attribute contains empty value.", containerEle); + } + } + containerDef.getPropertyValues().addPropertyValue("resourceAdapter", + new RuntimeBeanReference(resourceAdapterBeanName)); + + String activationSpecFactoryBeanName = containerEle.getAttribute(ACTIVATION_SPEC_FACTORY_ATTRIBUTE); + String destinationResolverBeanName = containerEle.getAttribute(DESTINATION_RESOLVER_ATTRIBUTE); + if (StringUtils.hasText(activationSpecFactoryBeanName)) { + if (StringUtils.hasText(destinationResolverBeanName)) { + parserContext.getReaderContext().error("Specify either 'activation-spec-factory' or " + + "'destination-resolver', not both. If you define a dedicated JmsActivationSpecFactory bean, " + + "specify the custom DestinationResolver there (if possible).", containerEle); + } + containerDef.getPropertyValues().addPropertyValue("activationSpecFactory", + new RuntimeBeanReference(activationSpecFactoryBeanName)); + } + if (StringUtils.hasText(destinationResolverBeanName)) { + containerDef.getPropertyValues().addPropertyValue("destinationResolver", + new RuntimeBeanReference(destinationResolverBeanName)); + } + + 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().addPropertyValue("acknowledgeMode", acknowledgeMode); + } + + String transactionManagerBeanName = containerEle.getAttribute(TRANSACTION_MANAGER_ATTRIBUTE); + if (StringUtils.hasText(transactionManagerBeanName)) { + containerDef.getPropertyValues().addPropertyValue("transactionManager", + new RuntimeBeanReference(transactionManagerBeanName)); + } + + int[] concurrency = parseConcurrency(containerEle, parserContext); + if (concurrency != null) { + configDef.getPropertyValues().addPropertyValue("maxConcurrency", new Integer(concurrency[1])); + } + + String prefetch = containerEle.getAttribute(PREFETCH_ATTRIBUTE); + if (StringUtils.hasText(prefetch)) { + configDef.getPropertyValues().addPropertyValue("prefetchSize", new Integer(prefetch)); + } + + containerDef.getPropertyValues().addPropertyValue("activationSpecConfig", configDef); + + return containerDef; + } + + protected boolean indicatesPubSub(BeanDefinition containerDef) { + BeanDefinition configDef = + (BeanDefinition) containerDef.getPropertyValues().getPropertyValue("activationSpecConfig").getValue(); + return indicatesPubSubConfig(configDef); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java b/org.springframework.jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java new file mode 100644 index 00000000000..9a9e6903f46 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2008 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.Session; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; + +/** + * Parser for the JMS <listener-container> element. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + */ +class JmsListenerContainerParser extends AbstractListenerContainerParser { + + private static final String CONTAINER_TYPE_ATTRIBUTE = "container-type"; + + private static final String CONTAINER_CLASS_ATTRIBUTE = "container-class"; + + private static final String CONNECTION_FACTORY_ATTRIBUTE = "connection-factory"; + + private static final String TASK_EXECUTOR_ATTRIBUTE = "task-executor"; + + private static final String CACHE_ATTRIBUTE = "cache"; + + + protected BeanDefinition parseContainer(Element listenerEle, Element containerEle, ParserContext parserContext) { + RootBeanDefinition containerDef = new RootBeanDefinition(); + containerDef.setSource(parserContext.extractSource(containerEle)); + + parseListenerConfiguration(listenerEle, parserContext, containerDef); + parseContainerConfiguration(containerEle, parserContext, containerDef); + + String containerType = containerEle.getAttribute(CONTAINER_TYPE_ATTRIBUTE); + String containerClass = containerEle.getAttribute(CONTAINER_CLASS_ATTRIBUTE); + if (!"".equals(containerClass)) { + containerDef.setBeanClassName(containerClass); + } + else if ("".equals(containerType) || "default".equals(containerType)) { + containerDef.setBeanClassName("org.springframework.jms.listener.DefaultMessageListenerContainer"); + } + else if ("default102".equals(containerType)) { + containerDef.setBeanClassName("org.springframework.jms.listener.DefaultMessageListenerContainer102"); + } + else if ("simple".equals(containerType)) { + containerDef.setBeanClassName("org.springframework.jms.listener.SimpleMessageListenerContainer"); + } + else if ("simple102".equals(containerType)) { + containerDef.setBeanClassName("org.springframework.jms.listener.SimpleMessageListenerContainer102"); + } + else { + parserContext.getReaderContext().error( + "Invalid 'container-type' attribute: only \"default(102)\" and \"simple(102)\" 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); + } + } + containerDef.getPropertyValues().addPropertyValue("connectionFactory", + new RuntimeBeanReference(connectionFactoryBeanName)); + + String taskExecutorBeanName = containerEle.getAttribute(TASK_EXECUTOR_ATTRIBUTE); + if (StringUtils.hasText(taskExecutorBeanName)) { + containerDef.getPropertyValues().addPropertyValue("taskExecutor", + new RuntimeBeanReference(taskExecutorBeanName)); + } + + String destinationResolverBeanName = containerEle.getAttribute(DESTINATION_RESOLVER_ATTRIBUTE); + if (StringUtils.hasText(destinationResolverBeanName)) { + containerDef.getPropertyValues().addPropertyValue("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().addPropertyValue("cacheLevelName", "CACHE_" + cache.toUpperCase()); + } + } + + Integer acknowledgeMode = parseAcknowledgeMode(containerEle, parserContext); + if (acknowledgeMode != null) { + if (acknowledgeMode.intValue() == Session.SESSION_TRANSACTED) { + containerDef.getPropertyValues().addPropertyValue("sessionTransacted", Boolean.TRUE); + } + else { + containerDef.getPropertyValues().addPropertyValue("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().addPropertyValue("transactionManager", + new RuntimeBeanReference(transactionManagerBeanName)); + } + } + + int[] concurrency = parseConcurrency(containerEle, parserContext); + if (concurrency != null) { + if (containerType.startsWith("default")) { + containerDef.getPropertyValues().addPropertyValue("concurrentConsumers", new Integer(concurrency[0])); + containerDef.getPropertyValues().addPropertyValue("maxConcurrentConsumers", new Integer(concurrency[1])); + } + else { + containerDef.getPropertyValues().addPropertyValue("concurrentConsumers", new Integer(concurrency[1])); + } + } + + String prefetch = containerEle.getAttribute(PREFETCH_ATTRIBUTE); + if (StringUtils.hasText(prefetch)) { + if (containerType.startsWith("default")) { + containerDef.getPropertyValues().addPropertyValue("maxMessagesPerTask", new Integer(prefetch)); + } + } + + return containerDef; + } + + protected boolean indicatesPubSub(BeanDefinition containerDef) { + return indicatesPubSubConfig(containerDef); + } + + protected boolean indicatesJms102(BeanDefinition containerDef) { + return containerDef.getBeanClassName().endsWith("102"); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/config/JmsNamespaceHandler.java b/org.springframework.jms/src/main/java/org/springframework/jms/config/JmsNamespaceHandler.java new file mode 100644 index 00000000000..28182738773 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/config/JmsNamespaceHandler.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2007 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.xml.NamespaceHandlerSupport; + +/** + * A {@link org.springframework.beans.factory.xml.NamespaceHandler} + * for the JMS namespace. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 2.5 + */ +public class JmsNamespaceHandler extends NamespaceHandlerSupport { + + public void init() { + registerBeanDefinitionParser("listener-container", new JmsListenerContainerParser()); + registerBeanDefinitionParser("jca-listener-container", new JcaListenerContainerParser()); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/config/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/config/package.html new file mode 100644 index 00000000000..000bb8254a1 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/config/package.html @@ -0,0 +1,8 @@ + + + +Support package for declarative messaging configuration, +with XML schema being the primary configuration format. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/config/spring-jms-2.5.xsd b/org.springframework.jms/src/main/java/org/springframework/jms/config/spring-jms-2.5.xsd new file mode 100644 index 00000000000..03b9a5e3ed4 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/config/spring-jms-2.5.xsd @@ -0,0 +1,439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/CachedMessageConsumer.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/CachedMessageConsumer.java new file mode 100644 index 00000000000..83393ec5afd --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/CachedMessageConsumer.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2008 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.connection; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; +import javax.jms.Queue; +import javax.jms.QueueReceiver; +import javax.jms.Topic; +import javax.jms.TopicSubscriber; + +/** + * JMS MessageConsumer decorator that adapts all calls + * to a shared MessageConsumer instance underneath. + * + * @author Juergen Hoeller + * @since 2.5.6 + */ +class CachedMessageConsumer implements MessageConsumer, QueueReceiver, TopicSubscriber { + + protected final MessageConsumer target; + + + public CachedMessageConsumer(MessageConsumer target) { + this.target = target; + } + + + public String getMessageSelector() throws JMSException { + return this.target.getMessageSelector(); + } + + public Queue getQueue() throws JMSException { + return (this.target instanceof QueueReceiver ? ((QueueReceiver) this.target).getQueue() : null); + } + + public Topic getTopic() throws JMSException { + return (this.target instanceof TopicSubscriber ? ((TopicSubscriber) this.target).getTopic() : null); + } + + public boolean getNoLocal() throws JMSException { + return (this.target instanceof TopicSubscriber && ((TopicSubscriber) this.target).getNoLocal()); + } + + public MessageListener getMessageListener() throws JMSException { + return this.target.getMessageListener(); + } + + public void setMessageListener(MessageListener messageListener) throws JMSException { + this.target.setMessageListener(messageListener); + } + + public Message receive() throws JMSException { + return this.target.receive(); + } + + public Message receive(long timeout) throws JMSException { + return this.target.receive(timeout); + } + + public Message receiveNoWait() throws JMSException { + return this.target.receiveNoWait(); + } + + public void close() throws JMSException { + // It's a cached MessageConsumer... + } + + + public String toString() { + return "Cached JMS MessageConsumer: " + this.target; + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java new file mode 100644 index 00000000000..34726e78bb4 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java @@ -0,0 +1,173 @@ +/* + * Copyright 2002-2008 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.connection; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.QueueSender; +import javax.jms.Topic; +import javax.jms.TopicPublisher; + +/** + * JMS MessageProducer decorator that adapts calls to a shared MessageProducer + * instance underneath, managing QoS settings locally within the decorator. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +class CachedMessageProducer implements MessageProducer, QueueSender, TopicPublisher { + + private final MessageProducer target; + + private Boolean originalDisableMessageID; + + private Boolean originalDisableMessageTimestamp; + + private int deliveryMode; + + private int priority; + + private long timeToLive; + + + public CachedMessageProducer(MessageProducer target) throws JMSException { + this.target = target; + this.deliveryMode = target.getDeliveryMode(); + this.priority = target.getPriority(); + this.timeToLive = target.getTimeToLive(); + } + + + public void setDisableMessageID(boolean disableMessageID) throws JMSException { + if (this.originalDisableMessageID == null) { + this.originalDisableMessageID = Boolean.valueOf(this.target.getDisableMessageID()); + } + this.target.setDisableMessageID(disableMessageID); + } + + public boolean getDisableMessageID() throws JMSException { + return this.target.getDisableMessageID(); + } + + public void setDisableMessageTimestamp(boolean disableMessageTimestamp) throws JMSException { + if (this.originalDisableMessageTimestamp == null) { + this.originalDisableMessageTimestamp = Boolean.valueOf(this.target.getDisableMessageTimestamp()); + } + this.target.setDisableMessageTimestamp(disableMessageTimestamp); + } + + public boolean getDisableMessageTimestamp() throws JMSException { + return this.target.getDisableMessageTimestamp(); + } + + public void setDeliveryMode(int deliveryMode) { + this.deliveryMode = deliveryMode; + } + + public int getDeliveryMode() { + return this.deliveryMode; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public int getPriority() { + return this.priority; + } + + public void setTimeToLive(long timeToLive) { + this.timeToLive = timeToLive; + } + + public long getTimeToLive() { + return this.timeToLive; + } + + public Destination getDestination() throws JMSException { + return this.target.getDestination(); + } + + public Queue getQueue() throws JMSException { + return (Queue) this.target.getDestination(); + } + + public Topic getTopic() throws JMSException { + return (Topic) this.target.getDestination(); + } + + public void send(Message message) throws JMSException { + this.target.send(message, this.deliveryMode, this.priority, this.timeToLive); + } + + public void send(Message message, int deliveryMode, int priority, long timeToLive) throws JMSException { + this.target.send(message, deliveryMode, priority, timeToLive); + } + + public void send(Destination destination, Message message) throws JMSException { + this.target.send(destination, message, this.deliveryMode, this.priority, this.timeToLive); + } + + public void send(Destination destination, Message message, int deliveryMode, int priority, long timeToLive) throws JMSException { + this.target.send(destination, message, deliveryMode, priority, timeToLive); + } + + public void send(Queue queue, Message message) throws JMSException { + this.target.send(queue, message, this.deliveryMode, this.priority, this.timeToLive); + } + + public void send(Queue queue, Message message, int deliveryMode, int priority, long timeToLive) throws JMSException { + this.target.send(queue, message, deliveryMode, priority, timeToLive); + } + + public void publish(Message message) throws JMSException { + this.target.send(message, this.deliveryMode, this.priority, this.timeToLive); + } + + public void publish(Message message, int deliveryMode, int priority, long timeToLive) throws JMSException { + this.target.send(message, deliveryMode, priority, timeToLive); + } + + public void publish(Topic topic, Message message) throws JMSException { + this.target.send(topic, message, this.deliveryMode, this.priority, this.timeToLive); + } + + public void publish(Topic topic, Message message, int deliveryMode, int priority, long timeToLive) throws JMSException { + this.target.send(topic, message, deliveryMode, priority, timeToLive); + } + + public void close() throws JMSException { + // It's a cached MessageProducer... reset properties only. + if (this.originalDisableMessageID != null) { + this.target.setDisableMessageID(this.originalDisableMessageID.booleanValue()); + this.originalDisableMessageID = null; + } + if (this.originalDisableMessageTimestamp != null) { + this.target.setDisableMessageTimestamp(this.originalDisableMessageTimestamp.booleanValue()); + this.originalDisableMessageTimestamp = null; + } + } + + + public String toString() { + return "Cached JMS MessageProducer: " + this.target; + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java new file mode 100644 index 00000000000..483507733a6 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -0,0 +1,468 @@ +/* + * Copyright 2002-2008 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.connection; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicSession; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * {@link SingleConnectionFactory} subclass that adds {@link javax.jms.Session} + * caching as well {@link javax.jms.MessageProducer} caching. This ConnectionFactory + * also switches the {@link #setReconnectOnException "reconnectOnException" property} + * to "true" by default, allowing for automatic recovery of the underlying Connection. + * + *

By default, only one single Session will be cached, with further requested + * Sessions being created and disposed on demand. Consider raising the + * {@link #setSessionCacheSize "sessionCacheSize" value} in case of a + * high-concurrency environment. + * + *

NOTE: This ConnectionFactory decorator requires JMS 1.1 or higher. + * You may use it through the JMS 1.0.2 API; however, the target JMS driver + * needs to be compliant with JMS 1.1. + * + *

When using the JMS 1.0.2 API, this ConnectionFactory will switch + * into queue/topic mode according to the JMS API methods used at runtime: + * createQueueConnection and createTopicConnection will + * lead to queue/topic mode, respectively; generic createConnection + * calls will lead to a JMS 1.1 connection which is able to serve both modes. + * + *

NOTE: This ConnectionFactory requires explicit closing of all Sessions + * obtained from its shared Connection. This is the usual recommendation for + * native JMS access code anyway. However, with this ConnectionFactory, its use + * is mandatory in order to actually allow for Session reuse. + * + *

Note also that MessageConsumers obtained from a cached Session won't get + * closed until the Session will eventually be removed from the pool. This may + * lead to semantic side effects in some cases. For a durable subscriber, the + * logical Session.close() call will also close the subscription. + * Re-registering a durable consumer for the same subscription on the same + * Session handle is not supported; close and reobtain a cached Session first. + * + * @author Juergen Hoeller + * @since 2.5.3 + */ +public class CachingConnectionFactory extends SingleConnectionFactory { + + private int sessionCacheSize = 1; + + private boolean cacheProducers = true; + + private boolean cacheConsumers = true; + + private volatile boolean active = true; + + private final Map cachedSessions = new HashMap(); + + + /** + * Create a new CachingConnectionFactory for bean-style usage. + * @see #setTargetConnectionFactory + */ + public CachingConnectionFactory() { + super(); + setReconnectOnException(true); + } + + /** + * Create a new CachingConnectionFactory for the given target + * ConnectionFactory. + * @param targetConnectionFactory the target ConnectionFactory + */ + public CachingConnectionFactory(ConnectionFactory targetConnectionFactory) { + super(targetConnectionFactory); + setReconnectOnException(true); + } + + + /** + * Specify the desired size for the JMS Session cache (per JMS Session type). + *

This cache size is the maximum limit for the number of cached Sessions + * per session acknowledgement type (auto, client, dups_ok, transacted). + * As a consequence, the actual number of cached Sessions may be up to + * four times as high as the specified value - in the unlikely case + * of mixing and matching different acknowledgement types. + *

Default is 1: caching a single Session, (re-)creating further ones on + * demand. Specify a number like 10 if you'd like to raise the number of cached + * Sessions; that said, 1 may be sufficient for low-concurrency scenarios. + * @see #setCacheProducers + */ + public void setSessionCacheSize(int sessionCacheSize) { + Assert.isTrue(sessionCacheSize >= 1, "Session cache size must be 1 or higher"); + this.sessionCacheSize = sessionCacheSize; + } + + /** + * Return the desired size for the JMS Session cache (per JMS Session type). + */ + public int getSessionCacheSize() { + return this.sessionCacheSize; + } + + /** + * Specify whether to cache JMS MessageProducers per JMS Session instance + * (more specifically: one MessageProducer per Destination and Session). + *

Default is "true". Switch this to "false" in order to always + * recreate MessageProducers on demand. + */ + public void setCacheProducers(boolean cacheProducers) { + this.cacheProducers = cacheProducers; + } + + /** + * Return whether to cache JMS MessageProducers per JMS Session instance. + */ + public boolean isCacheProducers() { + return this.cacheProducers; + } + + /** + * Specify whether to cache JMS MessageConsumers per JMS Session instance + * (more specifically: one MessageConsumer per Destination, selector String + * and Session). Note that durable subscribers will only be cached until + * logical closing of the Session handle. + *

Default is "true". Switch this to "false" in order to always + * recreate MessageConsumers on demand. + */ + public void setCacheConsumers(boolean cacheConsumers) { + this.cacheConsumers = cacheConsumers; + } + + /** + * Return whether to cache JMS MessageConsumers per JMS Session instance. + */ + public boolean isCacheConsumers() { + return this.cacheConsumers; + } + + + /** + * Resets the Session cache as well. + */ + public void resetConnection() { + this.active = false; + synchronized (this.cachedSessions) { + for (Iterator it = this.cachedSessions.values().iterator(); it.hasNext();) { + LinkedList sessionList = (LinkedList) it.next(); + synchronized (sessionList) { + for (Iterator it2 = sessionList.iterator(); it2.hasNext();) { + Session session = (Session) it2.next(); + try { + session.close(); + } + catch (Throwable ex) { + logger.trace("Could not close cached JMS Session", ex); + } + } + } + } + this.cachedSessions.clear(); + } + this.active = true; + + // Now proceed with actual closing of the shared Connection... + super.resetConnection(); + } + + /** + * Checks for a cached Session for the given mode. + */ + protected Session getSession(Connection con, Integer mode) throws JMSException { + LinkedList sessionList = null; + synchronized (this.cachedSessions) { + sessionList = (LinkedList) this.cachedSessions.get(mode); + if (sessionList == null) { + sessionList = new LinkedList(); + this.cachedSessions.put(mode, sessionList); + } + } + Session session = null; + synchronized (sessionList) { + if (!sessionList.isEmpty()) { + session = (Session) sessionList.removeFirst(); + } + } + if (session != null) { + if (logger.isTraceEnabled()) { + logger.trace("Found cached JMS Session for mode " + mode + ": " + + (session instanceof SessionProxy ? ((SessionProxy) session).getTargetSession() : session)); + } + } + else { + Session targetSession = createSession(con, mode); + if (logger.isDebugEnabled()) { + logger.debug("Creating cached JMS Session for mode " + mode + ": " + targetSession); + } + session = getCachedSessionProxy(targetSession, sessionList); + } + return session; + } + + /** + * Wrap the given Session with a proxy that delegates every method call to it + * but adapts close calls. This is useful for allowing application code to + * handle a special framework Session just like an ordinary Session. + * @param target the original Session to wrap + * @param sessionList the List of cached Sessions that the given Session belongs to + * @return the wrapped Session + */ + protected Session getCachedSessionProxy(Session target, LinkedList sessionList) { + List classes = new ArrayList(3); + classes.add(SessionProxy.class); + if (target instanceof QueueSession) { + classes.add(QueueSession.class); + } + if (target instanceof TopicSession) { + classes.add(TopicSession.class); + } + return (Session) Proxy.newProxyInstance( + SessionProxy.class.getClassLoader(), + (Class[]) classes.toArray(new Class[classes.size()]), + new CachedSessionInvocationHandler(target, sessionList)); + } + + + /** + * Invocation handler for a cached JMS Session proxy. + */ + private class CachedSessionInvocationHandler implements InvocationHandler { + + private final Session target; + + private final LinkedList sessionList; + + private final Map cachedProducers = new HashMap(); + + private final Map cachedConsumers = new HashMap(); + + private boolean transactionOpen = false; + + public CachedSessionInvocationHandler(Session target, LinkedList sessionList) { + this.target = target; + this.sessionList = sessionList; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + if (methodName.equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (methodName.equals("hashCode")) { + // Use hashCode of Session proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (methodName.equals("toString")) { + return "Cached JMS Session: " + this.target; + } + else if (methodName.equals("close")) { + // Handle close method: don't pass the call on. + if (active) { + synchronized (this.sessionList) { + if (this.sessionList.size() < getSessionCacheSize()) { + logicalClose(proxy); + // Remain open in the session list. + return null; + } + } + } + // If we get here, we're supposed to shut down. + physicalClose(); + return null; + } + else if (methodName.equals("getTargetSession")) { + // Handle getTargetSession method: return underlying Session. + return this.target; + } + else if (methodName.equals("commit") || methodName.equals("rollback")) { + this.transactionOpen = false; + } + else { + this.transactionOpen = true; + if ((methodName.equals("createProducer") || methodName.equals("createSender") || + methodName.equals("createPublisher")) && isCacheProducers()) { + return getCachedProducer((Destination) args[0]); + } + else if ((methodName.equals("createConsumer") || methodName.equals("createReceiver") || + methodName.equals("createSubscriber")) && isCacheConsumers()) { + return getCachedConsumer((Destination) args[0], (args.length > 1 ? (String) args[1] : null), + (args.length > 2 && ((Boolean) args[2]).booleanValue()), null); + } + else if (methodName.equals("createDurableSubscriber") && isCacheConsumers()) { + return getCachedConsumer((Destination) args[0], (args.length > 2 ? (String) args[2] : null), + (args.length > 3 && ((Boolean) args[3]).booleanValue()), (String) args[1]); + } + } + try { + return method.invoke(this.target, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + + private MessageProducer getCachedProducer(Destination dest) throws JMSException { + MessageProducer producer = (MessageProducer) this.cachedProducers.get(dest); + if (producer != null) { + if (logger.isTraceEnabled()) { + logger.trace("Found cached JMS MessageProducer for destination [" + dest + "]: " + producer); + } + } + else { + producer = this.target.createProducer(dest); + if (logger.isDebugEnabled()) { + logger.debug("Creating cached JMS MessageProducer for destination [" + dest + "]: " + producer); + } + this.cachedProducers.put(dest, producer); + } + return new CachedMessageProducer(producer); + } + + private MessageConsumer getCachedConsumer( + Destination dest, String selector, boolean noLocal, String subscription) throws JMSException { + + Object cacheKey = new ConsumerCacheKey(dest, selector, noLocal, subscription); + MessageConsumer consumer = (MessageConsumer) this.cachedConsumers.get(cacheKey); + if (consumer != null) { + if (logger.isTraceEnabled()) { + logger.trace("Found cached JMS MessageConsumer for destination [" + dest + "]: " + consumer); + } + } + else { + if (dest instanceof Topic) { + consumer = (subscription != null ? + this.target.createDurableSubscriber((Topic) dest, subscription, selector, noLocal) : + this.target.createConsumer(dest, selector, noLocal)); + } + else { + consumer = this.target.createConsumer(dest, selector); + } + if (logger.isDebugEnabled()) { + logger.debug("Creating cached JMS MessageConsumer for destination [" + dest + "]: " + consumer); + } + this.cachedConsumers.put(cacheKey, consumer); + } + return new CachedMessageConsumer(consumer); + } + + private void logicalClose(Object proxy) throws JMSException { + // Preserve rollback-on-close semantics. + if (this.transactionOpen && this.target.getTransacted()) { + this.transactionOpen = false; + this.target.rollback(); + } + // Physically close durable subscribers at time of Session close call. + for (Iterator it = this.cachedConsumers.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + ConsumerCacheKey key = (ConsumerCacheKey) entry.getKey(); + if (key.subscription != null) { + ((MessageConsumer) entry.getValue()).close(); + it.remove(); + } + } + // Allow for multiple close calls... + if (!this.sessionList.contains(proxy)) { + if (logger.isTraceEnabled()) { + logger.trace("Returning cached Session: " + this.target); + } + this.sessionList.addLast(proxy); + } + } + + private void physicalClose() throws JMSException { + if (logger.isDebugEnabled()) { + logger.debug("Closing cached Session: " + this.target); + } + // Explicitly close all MessageProducers and MessageConsumers that + // this Session happens to cache... + try { + for (Iterator it = this.cachedProducers.values().iterator(); it.hasNext();) { + ((MessageProducer) it.next()).close(); + } + for (Iterator it = this.cachedConsumers.values().iterator(); it.hasNext();) { + ((MessageConsumer) it.next()).close(); + } + } + finally { + this.cachedProducers.clear(); + this.cachedConsumers.clear(); + // Now actually close the Session. + this.target.close(); + } + } + } + + + /** + * Simple wrapper class around a Destination and other consumer attributes. + * Used as the key when caching consumers. + */ + private static class ConsumerCacheKey { + + private final Destination destination; + + private final String selector; + + private final boolean noLocal; + + private final String subscription; + + private ConsumerCacheKey(Destination destination, String selector, boolean noLocal, String subscription) { + this.destination = destination; + this.selector = selector; + this.noLocal = noLocal; + this.subscription = subscription; + } + + public boolean equals(Object other) { + if (other == this) { + return true; + } + ConsumerCacheKey otherKey = (ConsumerCacheKey) other; + return (this.destination.equals(otherKey.destination) && + ObjectUtils.nullSafeEquals(this.selector, otherKey.selector) && + this.noLocal == otherKey.noLocal && + ObjectUtils.nullSafeEquals(this.subscription, otherKey.subscription)); + } + + public int hashCode() { + return this.destination.hashCode(); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/ChainedExceptionListener.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/ChainedExceptionListener.java new file mode 100644 index 00000000000..e2bbf0dd5c0 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/ChainedExceptionListener.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2006 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.connection; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import javax.jms.ExceptionListener; +import javax.jms.JMSException; + +import org.springframework.util.Assert; + +/** + * Implementation of the JMS ExceptionListener interface that supports chaining, + * allowing the addition of multiple ExceptionListener instances in order. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class ChainedExceptionListener implements ExceptionListener { + + /** List of ExceptionListeners */ + private final List delegates = new ArrayList(2); + + + /** + * Add an ExceptionListener to the chained delegate list. + */ + public final void addDelegate(ExceptionListener listener) { + Assert.notNull(listener, "ExceptionListener must not be null"); + this.delegates.add(listener); + } + + /** + * Return all registered ExceptionListener delegates (as array). + */ + public final ExceptionListener[] getDelegates() { + return (ExceptionListener[]) this.delegates.toArray(new ExceptionListener[this.delegates.size()]); + } + + + public void onException(JMSException ex) { + for (Iterator it = this.delegates.iterator(); it.hasNext();) { + ExceptionListener listener = (ExceptionListener) it.next(); + listener.onException(ex); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/ConnectionFactoryUtils.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/ConnectionFactoryUtils.java new file mode 100644 index 00000000000..c47e407d0c1 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/ConnectionFactoryUtils.java @@ -0,0 +1,417 @@ +/* + * Copyright 2002-2008 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.connection; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; +import javax.jms.TopicSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.transaction.support.ResourceHolder; +import org.springframework.transaction.support.ResourceHolderSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Helper class for managing a JMS {@link javax.jms.ConnectionFactory}, in particular + * for obtaining transactional JMS resources for a given ConnectionFactory. + * + *

Mainly for internal use within the framework. Used by + * {@link org.springframework.jms.core.JmsTemplate} as well as + * {@link org.springframework.jms.listener.DefaultMessageListenerContainer}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see SmartConnectionFactory + */ +public abstract class ConnectionFactoryUtils { + + private static final Log logger = LogFactory.getLog(ConnectionFactoryUtils.class); + + + /** + * Release the given Connection, stopping it (if necessary) and eventually closing it. + *

Checks {@link SmartConnectionFactory#shouldStop}, if available. + * This is essentially a more sophisticated version of + * {@link org.springframework.jms.support.JmsUtils#closeConnection}. + * @param con the Connection to release + * (if this is null, the call will be ignored) + * @param cf the ConnectionFactory that the Connection was obtained from + * (may be null) + * @param started whether the Connection might have been started by the application + * @see SmartConnectionFactory#shouldStop + * @see org.springframework.jms.support.JmsUtils#closeConnection + */ + public static void releaseConnection(Connection con, ConnectionFactory cf, boolean started) { + if (con == null) { + return; + } + if (started && cf instanceof SmartConnectionFactory && ((SmartConnectionFactory) cf).shouldStop(con)) { + try { + con.stop(); + } + catch (Throwable ex) { + logger.debug("Could not stop JMS Connection before closing it", ex); + } + } + try { + con.close(); + } + catch (Throwable ex) { + logger.debug("Could not close JMS Connection", ex); + } + } + + /** + * Return the innermost target Session of the given Session. If the given + * Session is a proxy, it will be unwrapped until a non-proxy Session is + * found. Otherwise, the passed-in Session will be returned as-is. + * @param session the Session proxy to unwrap + * @return the innermost target Session, or the passed-in one if no proxy + * @see SessionProxy#getTargetSession() + */ + public static Session getTargetSession(Session session) { + Session sessionToUse = session; + while (sessionToUse instanceof SessionProxy) { + sessionToUse = ((SessionProxy) sessionToUse).getTargetSession(); + } + return sessionToUse; + } + + + + /** + * Determine whether the given JMS Session is transactional, that is, + * bound to the current thread by Spring's transaction facilities. + * @param session the JMS Session to check + * @param cf the JMS ConnectionFactory that the Session originated from + * @return whether the Session is transactional + */ + public static boolean isSessionTransactional(Session session, ConnectionFactory cf) { + if (session == null || cf == null) { + return false; + } + JmsResourceHolder resourceHolder = (JmsResourceHolder) TransactionSynchronizationManager.getResource(cf); + return (resourceHolder != null && resourceHolder.containsSession(session)); + } + + + /** + * Obtain a JMS Session that is synchronized with the current transaction, if any. + * @param cf the ConnectionFactory to obtain a Session for + * @param existingCon the existing JMS Connection to obtain a Session for + * (may be null) + * @param synchedLocalTransactionAllowed whether to allow for a local JMS transaction + * that is synchronized with a Spring-managed transaction (where the main transaction + * might be a JDBC-based one for a specific DataSource, for example), with the JMS + * transaction committing right after the main transaction. If not allowed, the given + * ConnectionFactory needs to handle transaction enlistment underneath the covers. + * @return the transactional Session, or null if none found + * @throws JMSException in case of JMS failure + */ + public static Session getTransactionalSession( + final ConnectionFactory cf, final Connection existingCon, final boolean synchedLocalTransactionAllowed) + throws JMSException { + + return doGetTransactionalSession(cf, new ResourceFactory() { + public Session getSession(JmsResourceHolder holder) { + return holder.getSession(Session.class, existingCon); + } + public Connection getConnection(JmsResourceHolder holder) { + return (existingCon != null ? existingCon : holder.getConnection()); + } + public Connection createConnection() throws JMSException { + return cf.createConnection(); + } + public Session createSession(Connection con) throws JMSException { + return con.createSession(synchedLocalTransactionAllowed, Session.AUTO_ACKNOWLEDGE); + } + public boolean isSynchedLocalTransactionAllowed() { + return synchedLocalTransactionAllowed; + } + }, true); + } + + /** + * Obtain a JMS QueueSession that is synchronized with the current transaction, if any. + *

Mainly intended for use with the JMS 1.0.2 API. + * @param cf the ConnectionFactory to obtain a Session for + * @param existingCon the existing JMS Connection to obtain a Session for + * (may be null) + * @param synchedLocalTransactionAllowed whether to allow for a local JMS transaction + * that is synchronized with a Spring-managed transaction (where the main transaction + * might be a JDBC-based one for a specific DataSource, for example), with the JMS + * transaction committing right after the main transaction. If not allowed, the given + * ConnectionFactory needs to handle transaction enlistment underneath the covers. + * @return the transactional Session, or null if none found + * @throws JMSException in case of JMS failure + */ + public static QueueSession getTransactionalQueueSession( + final QueueConnectionFactory cf, final QueueConnection existingCon, final boolean synchedLocalTransactionAllowed) + throws JMSException { + + return (QueueSession) doGetTransactionalSession(cf, new ResourceFactory() { + public Session getSession(JmsResourceHolder holder) { + return holder.getSession(QueueSession.class, existingCon); + } + public Connection getConnection(JmsResourceHolder holder) { + return (existingCon != null ? existingCon : holder.getConnection(QueueConnection.class)); + } + public Connection createConnection() throws JMSException { + return cf.createQueueConnection(); + } + public Session createSession(Connection con) throws JMSException { + return ((QueueConnection) con).createQueueSession(synchedLocalTransactionAllowed, Session.AUTO_ACKNOWLEDGE); + } + public boolean isSynchedLocalTransactionAllowed() { + return synchedLocalTransactionAllowed; + } + }, true); + } + + /** + * Obtain a JMS TopicSession that is synchronized with the current transaction, if any. + *

Mainly intended for use with the JMS 1.0.2 API. + * @param cf the ConnectionFactory to obtain a Session for + * @param existingCon the existing JMS Connection to obtain a Session for + * (may be null) + * @param synchedLocalTransactionAllowed whether to allow for a local JMS transaction + * that is synchronized with a Spring-managed transaction (where the main transaction + * might be a JDBC-based one for a specific DataSource, for example), with the JMS + * transaction committing right after the main transaction. If not allowed, the given + * ConnectionFactory needs to handle transaction enlistment underneath the covers. + * @return the transactional Session, or null if none found + * @throws JMSException in case of JMS failure + */ + public static TopicSession getTransactionalTopicSession( + final TopicConnectionFactory cf, final TopicConnection existingCon, final boolean synchedLocalTransactionAllowed) + throws JMSException { + + return (TopicSession) doGetTransactionalSession(cf, new ResourceFactory() { + public Session getSession(JmsResourceHolder holder) { + return holder.getSession(TopicSession.class, existingCon); + } + public Connection getConnection(JmsResourceHolder holder) { + return (existingCon != null ? existingCon : holder.getConnection(TopicConnection.class)); + } + public Connection createConnection() throws JMSException { + return cf.createTopicConnection(); + } + public Session createSession(Connection con) throws JMSException { + return ((TopicConnection) con).createTopicSession(synchedLocalTransactionAllowed, Session.AUTO_ACKNOWLEDGE); + } + public boolean isSynchedLocalTransactionAllowed() { + return synchedLocalTransactionAllowed; + } + }, true); + } + + /** + * Obtain a JMS Session that is synchronized with the current transaction, if any. + *

This doGetTransactionalSession variant always starts the underlying + * JMS Connection, assuming that the Session will be used for receiving messages. + * @param connectionFactory the JMS ConnectionFactory to bind for + * (used as TransactionSynchronizationManager key) + * @param resourceFactory the ResourceFactory to use for extracting or creating + * JMS resources + * @return the transactional Session, or null if none found + * @throws JMSException in case of JMS failure + * @see #doGetTransactionalSession(javax.jms.ConnectionFactory, ResourceFactory, boolean) + */ + public static Session doGetTransactionalSession( + ConnectionFactory connectionFactory, ResourceFactory resourceFactory) throws JMSException { + + return doGetTransactionalSession(connectionFactory, resourceFactory, true); + } + + /** + * Obtain a JMS Session that is synchronized with the current transaction, if any. + * @param connectionFactory the JMS ConnectionFactory to bind for + * (used as TransactionSynchronizationManager key) + * @param resourceFactory the ResourceFactory to use for extracting or creating + * JMS resources + * @param startConnection whether the underlying JMS Connection approach should be + * started in order to allow for receiving messages. Note that a reused Connection + * may already have been started before, even if this flag is false. + * @return the transactional Session, or null if none found + * @throws JMSException in case of JMS failure + */ + public static Session doGetTransactionalSession( + ConnectionFactory connectionFactory, ResourceFactory resourceFactory, boolean startConnection) + throws JMSException { + + Assert.notNull(connectionFactory, "ConnectionFactory must not be null"); + Assert.notNull(resourceFactory, "ResourceFactory must not be null"); + + JmsResourceHolder resourceHolder = + (JmsResourceHolder) TransactionSynchronizationManager.getResource(connectionFactory); + if (resourceHolder != null) { + Session session = resourceFactory.getSession(resourceHolder); + if (session != null) { + if (startConnection) { + Connection con = resourceFactory.getConnection(resourceHolder); + if (con != null) { + con.start(); + } + } + return session; + } + if (resourceHolder.isFrozen()) { + return null; + } + } + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + return null; + } + JmsResourceHolder resourceHolderToUse = resourceHolder; + if (resourceHolderToUse == null) { + resourceHolderToUse = new JmsResourceHolder(connectionFactory); + } + Connection con = resourceFactory.getConnection(resourceHolderToUse); + Session session = null; + try { + boolean isExistingCon = (con != null); + if (!isExistingCon) { + con = resourceFactory.createConnection(); + resourceHolderToUse.addConnection(con); + } + session = resourceFactory.createSession(con); + resourceHolderToUse.addSession(session, con); + if (startConnection) { + con.start(); + } + } + catch (JMSException ex) { + if (session != null) { + try { + session.close(); + } + catch (Throwable ex2) { + // ignore + } + } + if (con != null) { + try { + con.close(); + } + catch (Throwable ex2) { + // ignore + } + } + throw ex; + } + if (resourceHolderToUse != resourceHolder) { + TransactionSynchronizationManager.registerSynchronization( + new JmsResourceSynchronization( + resourceHolderToUse, connectionFactory, resourceFactory.isSynchedLocalTransactionAllowed())); + resourceHolderToUse.setSynchronizedWithTransaction(true); + TransactionSynchronizationManager.bindResource(connectionFactory, resourceHolderToUse); + } + return session; + } + + + /** + * Callback interface for resource creation. + * Serving as argument for the doGetTransactionalSession method. + */ + public interface ResourceFactory { + + /** + * Fetch an appropriate Session from the given JmsResourceHolder. + * @param holder the JmsResourceHolder + * @return an appropriate Session fetched from the holder, + * or null if none found + */ + Session getSession(JmsResourceHolder holder); + + /** + * Fetch an appropriate Connection from the given JmsResourceHolder. + * @param holder the JmsResourceHolder + * @return an appropriate Connection fetched from the holder, + * or null if none found + */ + Connection getConnection(JmsResourceHolder holder); + + /** + * Create a new JMS Connection for registration with a JmsResourceHolder. + * @return the new JMS Connection + * @throws JMSException if thrown by JMS API methods + */ + Connection createConnection() throws JMSException; + + /** + * Create a new JMS Session for registration with a JmsResourceHolder. + * @param con the JMS Connection to create a Session for + * @return the new JMS Session + * @throws JMSException if thrown by JMS API methods + */ + Session createSession(Connection con) throws JMSException; + + /** + * Return whether to allow for a local JMS transaction that is synchronized with + * a Spring-managed transaction (where the main transaction might be a JDBC-based + * one for a specific DataSource, for example), with the JMS transaction + * committing right after the main transaction. + * @return whether to allow for synchronizing a local JMS transaction + */ + boolean isSynchedLocalTransactionAllowed(); + } + + + /** + * Callback for resource cleanup at the end of a non-native JMS transaction + * (e.g. when participating in a JtaTransactionManager transaction). + * @see org.springframework.transaction.jta.JtaTransactionManager + */ + private static class JmsResourceSynchronization extends ResourceHolderSynchronization { + + private final boolean transacted; + + public JmsResourceSynchronization(JmsResourceHolder resourceHolder, Object resourceKey, boolean transacted) { + super(resourceHolder, resourceKey); + this.transacted = transacted; + } + + protected boolean shouldReleaseBeforeCompletion() { + return !this.transacted; + } + + protected void processResourceAfterCommit(ResourceHolder resourceHolder) { + try { + ((JmsResourceHolder) resourceHolder).commitAll(); + } + catch (JMSException ex) { + throw new SynchedLocalTransactionFailedException("Local JMS transaction failed to commit", ex); + } + } + + protected void releaseResource(ResourceHolder resourceHolder, Object resourceKey) { + ((JmsResourceHolder) resourceHolder).closeAll(); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/DelegatingConnectionFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/DelegatingConnectionFactory.java new file mode 100644 index 00000000000..9fd0e8a3c2b --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/DelegatingConnectionFactory.java @@ -0,0 +1,163 @@ +/* + * Copyright 2002-2007 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.connection; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * {@link javax.jms.ConnectionFactory} implementation that delegates all calls + * to a given target {@link javax.jms.ConnectionFactory}, adapting specific + * create(Queue/Topic)Connection calls to the target ConnectionFactory + * if necessary (e.g. when running JMS 1.0.2 API based code against a generic + * JMS 1.1 ConnectionFactory, such as ActiveMQ's PooledConnectionFactory). + * + *

This class allows for being subclassed, with subclasses overriding only + * those methods (such as {@link #createConnection()}) that should not simply + * delegate to the target ConnectionFactory. + * + *

Can also be defined as-is, wrapping a specific target ConnectionFactory, + * using the "shouldStopConnections" flag to indicate whether Connections + * obtained from the target factory are supposed to be stopped before closed. + * The latter may be necessary for some connection pools that simply return + * released connections to the pool, not stopping them while they sit in the pool. + * + * @author Juergen Hoeller + * @since 2.0.2 + * @see #createConnection() + * @see #setShouldStopConnections + * @see ConnectionFactoryUtils#releaseConnection + */ +public class DelegatingConnectionFactory + implements SmartConnectionFactory, QueueConnectionFactory, TopicConnectionFactory, InitializingBean { + + private ConnectionFactory targetConnectionFactory; + + private boolean shouldStopConnections = false; + + + /** + * Set the target ConnectionFactory that this ConnectionFactory should delegate to. + */ + public void setTargetConnectionFactory(ConnectionFactory targetConnectionFactory) { + Assert.notNull(targetConnectionFactory, "'targetConnectionFactory' must not be null"); + this.targetConnectionFactory = targetConnectionFactory; + } + + /** + * Return the target ConnectionFactory that this ConnectionFactory delegates to. + */ + public ConnectionFactory getTargetConnectionFactory() { + return this.targetConnectionFactory; + } + + /** + * Indicate whether Connections obtained from the target factory are supposed + * to be stopped before closed ("true") or simply closed ("false"). + * The latter may be necessary for some connection pools that simply return + * released connections to the pool, not stopping them while they sit in the pool. + *

Default is "false", simply closing Connections. + * @see ConnectionFactoryUtils#releaseConnection + */ + public void setShouldStopConnections(boolean shouldStopConnections) { + this.shouldStopConnections = shouldStopConnections; + } + + public void afterPropertiesSet() { + if (getTargetConnectionFactory() == null) { + throw new IllegalArgumentException("'targetConnectionFactory' is required"); + } + } + + + public Connection createConnection() throws JMSException { + return getTargetConnectionFactory().createConnection(); + } + + public Connection createConnection(String username, String password) throws JMSException { + return getTargetConnectionFactory().createConnection(username, password); + } + + public QueueConnection createQueueConnection() throws JMSException { + ConnectionFactory cf = getTargetConnectionFactory(); + if (cf instanceof QueueConnectionFactory) { + return ((QueueConnectionFactory) cf).createQueueConnection(); + } + else { + Connection con = cf.createConnection(); + if (!(con instanceof QueueConnection)) { + throw new javax.jms.IllegalStateException("'targetConnectionFactory' is not a QueueConnectionFactory"); + } + return (QueueConnection) con; + } + } + + public QueueConnection createQueueConnection(String username, String password) throws JMSException { + ConnectionFactory cf = getTargetConnectionFactory(); + if (cf instanceof QueueConnectionFactory) { + return ((QueueConnectionFactory) cf).createQueueConnection(username, password); + } + else { + Connection con = cf.createConnection(username, password); + if (!(con instanceof QueueConnection)) { + throw new javax.jms.IllegalStateException("'targetConnectionFactory' is not a QueueConnectionFactory"); + } + return (QueueConnection) con; + } + } + + public TopicConnection createTopicConnection() throws JMSException { + ConnectionFactory cf = getTargetConnectionFactory(); + if (cf instanceof TopicConnectionFactory) { + return ((TopicConnectionFactory) cf).createTopicConnection(); + } + else { + Connection con = cf.createConnection(); + if (!(con instanceof TopicConnection)) { + throw new javax.jms.IllegalStateException("'targetConnectionFactory' is not a TopicConnectionFactory"); + } + return (TopicConnection) con; + } + } + + public TopicConnection createTopicConnection(String username, String password) throws JMSException { + ConnectionFactory cf = getTargetConnectionFactory(); + if (cf instanceof TopicConnectionFactory) { + return ((TopicConnectionFactory) cf).createTopicConnection(username, password); + } + else { + Connection con = cf.createConnection(username, password); + if (!(con instanceof TopicConnection)) { + throw new javax.jms.IllegalStateException("'targetConnectionFactory' is not a TopicConnectionFactory"); + } + return (TopicConnection) con; + } + } + + public boolean shouldStop(Connection con) { + return this.shouldStopConnections; + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsResourceHolder.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsResourceHolder.java new file mode 100644 index 00000000000..94285541903 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsResourceHolder.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2008 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.connection; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.Session; +import javax.jms.TransactionInProgressException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * JMS resource holder, wrapping a JMS Connection and a JMS Session. + * JmsTransactionManager binds instances of this class to the thread, + * for a given JMS ConnectionFactory. + * + *

Note: This is an SPI class, not intended to be used by applications. + * + * @author Juergen Hoeller + * @since 1.1 + * @see JmsTransactionManager + * @see org.springframework.jms.core.JmsTemplate + */ +public class JmsResourceHolder extends ResourceHolderSupport { + + private static final Log logger = LogFactory.getLog(JmsResourceHolder.class); + + private ConnectionFactory connectionFactory; + + private boolean frozen = false; + + private final List connections = new LinkedList(); + + private final List sessions = new LinkedList(); + + private final Map sessionsPerConnection = new HashMap(); + + + /** + * Create a new JmsResourceHolder that is open for resources to be added. + * @see #addConnection + * @see #addSession + */ + public JmsResourceHolder() { + } + + /** + * Create a new JmsResourceHolder that is open for resources to be added. + * @param connectionFactory the JMS ConnectionFactory that this + * resource holder is associated with (may be null) + */ + public JmsResourceHolder(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + /** + * Create a new JmsResourceHolder for the given JMS Session. + * @param session the JMS Session + */ + public JmsResourceHolder(Session session) { + addSession(session); + this.frozen = true; + } + + /** + * Create a new JmsResourceHolder for the given JMS resources. + * @param connection the JMS Connection + * @param session the JMS Session + */ + public JmsResourceHolder(Connection connection, Session session) { + addConnection(connection); + addSession(session, connection); + this.frozen = true; + } + + /** + * Create a new JmsResourceHolder for the given JMS resources. + * @param connectionFactory the JMS ConnectionFactory that this + * resource holder is associated with (may be null) + * @param connection the JMS Connection + * @param session the JMS Session + */ + public JmsResourceHolder(ConnectionFactory connectionFactory, Connection connection, Session session) { + this.connectionFactory = connectionFactory; + addConnection(connection); + addSession(session, connection); + this.frozen = true; + } + + + public final boolean isFrozen() { + return this.frozen; + } + + public final void addConnection(Connection connection) { + Assert.isTrue(!this.frozen, "Cannot add Connection because JmsResourceHolder is frozen"); + Assert.notNull(connection, "Connection must not be null"); + if (!this.connections.contains(connection)) { + this.connections.add(connection); + } + } + + public final void addSession(Session session) { + addSession(session, null); + } + + public final void addSession(Session session, Connection connection) { + Assert.isTrue(!this.frozen, "Cannot add Session because JmsResourceHolder is frozen"); + Assert.notNull(session, "Session must not be null"); + if (!this.sessions.contains(session)) { + this.sessions.add(session); + if (connection != null) { + List sessions = (List) this.sessionsPerConnection.get(connection); + if (sessions == null) { + sessions = new LinkedList(); + this.sessionsPerConnection.put(connection, sessions); + } + sessions.add(session); + } + } + } + + public boolean containsSession(Session session) { + return this.sessions.contains(session); + } + + + public Connection getConnection() { + return (!this.connections.isEmpty() ? (Connection) this.connections.get(0) : null); + } + + public Connection getConnection(Class connectionType) { + return (Connection) CollectionUtils.findValueOfType(this.connections, connectionType); + } + + public Session getSession() { + return (!this.sessions.isEmpty() ? (Session) this.sessions.get(0) : null); + } + + public Session getSession(Class sessionType) { + return getSession(sessionType, null); + } + + public Session getSession(Class sessionType, Connection connection) { + List sessions = this.sessions; + if (connection != null) { + sessions = (List) this.sessionsPerConnection.get(connection); + } + return (Session) CollectionUtils.findValueOfType(sessions, sessionType); + } + + + public void commitAll() throws JMSException { + for (Iterator it = this.sessions.iterator(); it.hasNext();) { + try { + ((Session) it.next()).commit(); + } + catch (TransactionInProgressException ex) { + // Ignore -> can only happen in case of a JTA transaction. + } + catch (javax.jms.IllegalStateException ex) { + // Ignore -> can only happen in case of a JTA transaction. + } + } + } + + public void closeAll() { + for (Iterator it = this.sessions.iterator(); it.hasNext();) { + try { + ((Session) it.next()).close(); + } + catch (Throwable ex) { + logger.debug("Could not close synchronized JMS Session after transaction", ex); + } + } + for (Iterator it = this.connections.iterator(); it.hasNext();) { + Connection con = (Connection) it.next(); + ConnectionFactoryUtils.releaseConnection(con, this.connectionFactory, true); + } + this.connections.clear(); + this.sessions.clear(); + this.sessionsPerConnection.clear(); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager.java new file mode 100644 index 00000000000..a1842610678 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager.java @@ -0,0 +1,321 @@ +/* + * Copyright 2002-2008 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.connection; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.Session; +import javax.jms.TransactionRolledBackException; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.InvalidIsolationLevelException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.UnexpectedRollbackException; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.SmartTransactionObject; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * {@link org.springframework.transaction.PlatformTransactionManager} implementation + * for a single JMS {@link javax.jms.ConnectionFactory}. Binds a JMS + * Connection/Session pair from the specified ConnectionFactory to the thread, + * potentially allowing for one thread-bound Session per ConnectionFactory. + * + *

NOTE: This class requires a JMS 1.1+ provider because it builds on + * the domain-independent API. Use the {@link JmsTransactionManager102} subclass + * for a JMS 1.0.2 provider, e.g. when running on a J2EE 1.3 server. + * + *

This local strategy is an alternative to executing JMS operations within + * JTA transactions. Its advantage is that it is able to work in any environment, + * for example a standalone application or a test suite, with any message broker + * as target. However, this strategy is not able to provide XA transactions, + * for example in order to share transactions between messaging and database access. + * A full JTA/XA setup is required for XA transactions, typically using Spring's + * {@link org.springframework.transaction.jta.JtaTransactionManager} as strategy. + * + *

Application code is required to retrieve the transactional JMS Session via + * {@link ConnectionFactoryUtils#getTransactionalSession} instead of a standard + * J2EE-style {@link ConnectionFactory#createConnection()} call with subsequent + * Session creation. Spring's {@link org.springframework.jms.core.JmsTemplate} + * will autodetect a thread-bound Session and automatically participate in it. + * + *

Alternatively, you can allow application code to work with the standard + * J2EE-style lookup pattern on a ConnectionFactory, for example for legacy code + * that is not aware of Spring at all. In that case, define a + * {@link TransactionAwareConnectionFactoryProxy} for your target ConnectionFactory, + * which will automatically participate in Spring-managed transactions. + * + *

The use of {@link CachingConnectionFactory} as a target for this + * transaction manager is strongly recommended. CachingConnectionFactory + * uses a single JMS Connection for all JMS access in order to avoid the overhead + * of repeated Connection creation, as well as maintaining a cache of Sessions. + * Each transaction will then share the same JMS Connection, while still using + * its own individual JMS Session. + * + *

The use of a raw target ConnectionFactory would not only be inefficient + * because of the lack of resource reuse. It might also lead to strange effects + * when your JMS driver doesn't accept MessageProducer.close() calls + * and/or MessageConsumer.close() calls before Session.commit(), + * with the latter supposed to commit all the messages that have been sent through the + * producer handle and received through the consumer handle. As a safe general solution, + * always pass in a {@link CachingConnectionFactory} into this transaction manager's + * {@link #setConnectionFactory "connectionFactory"} property. + * + *

Transaction synchronization is turned off by default, as this manager might + * be used alongside a datastore-based Spring transaction manager such as the + * JDBC {@link org.springframework.jdbc.datasource.DataSourceTransactionManager}, + * which has stronger needs for synchronization. + * + * @author Juergen Hoeller + * @since 1.1 + * @see ConnectionFactoryUtils#getTransactionalSession + * @see TransactionAwareConnectionFactoryProxy + * @see org.springframework.jms.core.JmsTemplate + */ +public class JmsTransactionManager extends AbstractPlatformTransactionManager + implements ResourceTransactionManager, InitializingBean { + + private ConnectionFactory connectionFactory; + + + /** + * Create a new JmsTransactionManager for bean-style usage. + *

Note: The ConnectionFactory has to be set before using the instance. + * This constructor can be used to prepare a JmsTemplate via a BeanFactory, + * typically setting the ConnectionFactory via setConnectionFactory. + *

Turns off transaction synchronization by default, as this manager might + * be used alongside a datastore-based Spring transaction manager like + * DataSourceTransactionManager, which has stronger needs for synchronization. + * Only one manager is allowed to drive synchronization at any point of time. + * @see #setConnectionFactory + * @see #setTransactionSynchronization + */ + public JmsTransactionManager() { + setTransactionSynchronization(SYNCHRONIZATION_NEVER); + } + + /** + * Create a new JmsTransactionManager, given a ConnectionFactory. + * @param connectionFactory the ConnectionFactory to obtain connections from + */ + public JmsTransactionManager(ConnectionFactory connectionFactory) { + this(); + setConnectionFactory(connectionFactory); + afterPropertiesSet(); + } + + + /** + * Set the JMS ConnectionFactory that this instance should manage transactions for. + */ + public void setConnectionFactory(ConnectionFactory cf) { + if (cf instanceof TransactionAwareConnectionFactoryProxy) { + // If we got a TransactionAwareConnectionFactoryProxy, we need to perform transactions + // for its underlying target ConnectionFactory, else JMS access code won't see + // properly exposed transactions (i.e. transactions for the target ConnectionFactory). + this.connectionFactory = ((TransactionAwareConnectionFactoryProxy) cf).getTargetConnectionFactory(); + } + else { + this.connectionFactory = cf; + } + } + + /** + * Return the JMS ConnectionFactory that this instance should manage transactions for. + */ + public ConnectionFactory getConnectionFactory() { + return this.connectionFactory; + } + + /** + * Make sure the ConnectionFactory has been set. + */ + public void afterPropertiesSet() { + if (getConnectionFactory() == null) { + throw new IllegalArgumentException("Property 'connectionFactory' is required"); + } + } + + + public Object getResourceFactory() { + return getConnectionFactory(); + } + + protected Object doGetTransaction() { + JmsTransactionObject txObject = new JmsTransactionObject(); + txObject.setResourceHolder( + (JmsResourceHolder) TransactionSynchronizationManager.getResource(getConnectionFactory())); + return txObject; + } + + protected boolean isExistingTransaction(Object transaction) { + JmsTransactionObject txObject = (JmsTransactionObject) transaction; + return (txObject.getResourceHolder() != null); + } + + protected void doBegin(Object transaction, TransactionDefinition definition) { + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + throw new InvalidIsolationLevelException("JMS does not support an isolation level concept"); + } + JmsTransactionObject txObject = (JmsTransactionObject) transaction; + Connection con = null; + Session session = null; + try { + con = createConnection(); + session = createSession(con); + if (logger.isDebugEnabled()) { + logger.debug("Created JMS transaction on Session [" + session + "] from Connection [" + con + "]"); + } + txObject.setResourceHolder(new JmsResourceHolder(getConnectionFactory(), con, session)); + txObject.getResourceHolder().setSynchronizedWithTransaction(true); + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + txObject.getResourceHolder().setTimeoutInSeconds(timeout); + } + TransactionSynchronizationManager.bindResource( + getConnectionFactory(), txObject.getResourceHolder()); + } + catch (JMSException ex) { + if (session != null) { + try { + session.close(); + } + catch (Throwable ex2) { + // ignore + } + } + if (con != null) { + try { + con.close(); + } + catch (Throwable ex2) { + // ignore + } + } + throw new CannotCreateTransactionException("Could not create JMS transaction", ex); + } + } + + protected Object doSuspend(Object transaction) { + JmsTransactionObject txObject = (JmsTransactionObject) transaction; + txObject.setResourceHolder(null); + return TransactionSynchronizationManager.unbindResource(getConnectionFactory()); + } + + protected void doResume(Object transaction, Object suspendedResources) { + JmsResourceHolder conHolder = (JmsResourceHolder) suspendedResources; + TransactionSynchronizationManager.bindResource(getConnectionFactory(), conHolder); + } + + protected void doCommit(DefaultTransactionStatus status) { + JmsTransactionObject txObject = (JmsTransactionObject) status.getTransaction(); + Session session = txObject.getResourceHolder().getSession(); + try { + if (status.isDebug()) { + logger.debug("Committing JMS transaction on Session [" + session + "]"); + } + session.commit(); + } + catch (TransactionRolledBackException ex) { + throw new UnexpectedRollbackException("JMS transaction rolled back", ex); + } + catch (JMSException ex) { + throw new TransactionSystemException("Could not commit JMS transaction", ex); + } + } + + protected void doRollback(DefaultTransactionStatus status) { + JmsTransactionObject txObject = (JmsTransactionObject) status.getTransaction(); + Session session = txObject.getResourceHolder().getSession(); + try { + if (status.isDebug()) { + logger.debug("Rolling back JMS transaction on Session [" + session + "]"); + } + session.rollback(); + } + catch (JMSException ex) { + throw new TransactionSystemException("Could not roll back JMS transaction", ex); + } + } + + protected void doSetRollbackOnly(DefaultTransactionStatus status) { + JmsTransactionObject txObject = (JmsTransactionObject) status.getTransaction(); + txObject.getResourceHolder().setRollbackOnly(); + } + + protected void doCleanupAfterCompletion(Object transaction) { + JmsTransactionObject txObject = (JmsTransactionObject) transaction; + TransactionSynchronizationManager.unbindResource(getConnectionFactory()); + txObject.getResourceHolder().closeAll(); + txObject.getResourceHolder().clear(); + } + + + //------------------------------------------------------------------------- + // JMS 1.1 factory methods, potentially overridden for JMS 1.0.2 + //------------------------------------------------------------------------- + + /** + * Create a JMS Connection via this template's ConnectionFactory. + *

This implementation uses JMS 1.1 API. + * @return the new JMS Connection + * @throws javax.jms.JMSException if thrown by JMS API methods + */ + protected Connection createConnection() throws JMSException { + return getConnectionFactory().createConnection(); + } + + /** + * Create a JMS Session for the given Connection. + *

This implementation uses JMS 1.1 API. + * @param con the JMS Connection to create a Session for + * @return the new JMS Session + * @throws javax.jms.JMSException if thrown by JMS API methods + */ + protected Session createSession(Connection con) throws JMSException { + return con.createSession(true, Session.AUTO_ACKNOWLEDGE); + } + + + /** + * JMS transaction object, representing a JmsResourceHolder. + * Used as transaction object by JmsTransactionManager. + * @see JmsResourceHolder + */ + private static class JmsTransactionObject implements SmartTransactionObject { + + private JmsResourceHolder resourceHolder; + + public void setResourceHolder(JmsResourceHolder resourceHolder) { + this.resourceHolder = resourceHolder; + } + + public JmsResourceHolder getResourceHolder() { + return this.resourceHolder; + } + + public boolean isRollbackOnly() { + return this.resourceHolder.isRollbackOnly(); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager102.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager102.java new file mode 100644 index 00000000000..8efdac42073 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager102.java @@ -0,0 +1,148 @@ +/* + * Copyright 2002-2007 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.connection; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.Session; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; + +/** + * A subclass of {@link JmsTransactionManager} for the JMS 1.0.2 specification, + * not relying on JMS 1.1 methods like JmsTransactionManager itself. + * This class can be used for JMS 1.0.2 providers, offering the same API as + * JmsTransactionManager does for JMS 1.1 providers. + * + *

You need to set the {@link #setPubSubDomain "pubSubDomain" property}, + * since this class will always explicitly differentiate between a + * {@link javax.jms.QueueConnection} and a {@link javax.jms.TopicConnection}. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setConnectionFactory + * @see #setPubSubDomain + */ +public class JmsTransactionManager102 extends JmsTransactionManager { + + private boolean pubSubDomain = false; + + + /** + * Create a new JmsTransactionManager102 for bean-style usage. + *

Note: The ConnectionFactory has to be set before using the instance. + * This constructor can be used to prepare a JmsTemplate via a BeanFactory, + * typically setting the ConnectionFactory via setConnectionFactory. + * @see #setConnectionFactory + */ + public JmsTransactionManager102() { + super(); + } + + /** + * Create a new JmsTransactionManager102, given a ConnectionFactory. + * @param connectionFactory the ConnectionFactory to manage transactions for + * @param pubSubDomain whether the Publish/Subscribe domain (Topics) or + * Point-to-Point domain (Queues) should be used + * @see #setPubSubDomain + */ + public JmsTransactionManager102(ConnectionFactory connectionFactory, boolean pubSubDomain) { + setConnectionFactory(connectionFactory); + this.pubSubDomain = pubSubDomain; + afterPropertiesSet(); + } + + + /** + * Configure the transaction manager with knowledge of the JMS domain used. + * This tells the JMS 1.0.2 provider which class hierarchy to use for creating + * Connections and Sessions. + *

Default is Point-to-Point (Queues). + * @param pubSubDomain true for Publish/Subscribe domain (Topics), + * false for Point-to-Point domain (Queues) + */ + public void setPubSubDomain(boolean pubSubDomain) { + this.pubSubDomain = pubSubDomain; + } + + /** + * Return whether the Publish/Subscribe domain (Topics) is used. + * Otherwise, the Point-to-Point domain (Queues) is used. + */ + public boolean isPubSubDomain() { + return this.pubSubDomain; + } + + + /** + * In addition to checking if the connection factory is set, make sure + * that the supplied connection factory is of the appropriate type for + * the specified destination type: QueueConnectionFactory for queues, + * and TopicConnectionFactory for topics. + */ + public void afterPropertiesSet() { + super.afterPropertiesSet(); + + // Make sure that the ConnectionFactory passed is consistent. + // Some provider implementations of the ConnectionFactory interface + // implement both domain interfaces under the cover, so just check if + // the selected domain is consistent with the type of connection factory. + if (isPubSubDomain()) { + if (!(getConnectionFactory() instanceof TopicConnectionFactory)) { + throw new IllegalArgumentException( + "Specified a Spring JMS 1.0.2 transaction manager for topics " + + "but did not supply an instance of TopicConnectionFactory"); + } + } + else { + if (!(getConnectionFactory() instanceof QueueConnectionFactory)) { + throw new IllegalArgumentException( + "Specified a Spring JMS 1.0.2 transaction manager for queues " + + "but did not supply an instance of QueueConnectionFactory"); + } + } + } + + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Connection createConnection() throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnectionFactory) getConnectionFactory()).createTopicConnection(); + } + else { + return ((QueueConnectionFactory) getConnectionFactory()).createQueueConnection(); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Session createSession(Connection con) throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnection) con).createTopicSession(true, Session.AUTO_ACKNOWLEDGE); + } + else { + return ((QueueConnection) con).createQueueSession(true, Session.AUTO_ACKNOWLEDGE); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/SessionProxy.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/SessionProxy.java new file mode 100644 index 00000000000..d0708f58314 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/SessionProxy.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2008 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.connection; + +import javax.jms.Session; + +/** + * Subinterface of {@link javax.jms.Session} to be implemented by + * Session proxies. Allows access to the the underlying target Session. + * + * @author Juergen Hoeller + * @since 2.0.4 + * @see TransactionAwareConnectionFactoryProxy + * @see CachingConnectionFactory + * @see ConnectionFactoryUtils#getTargetSession(javax.jms.Session) + */ +public interface SessionProxy extends Session { + + /** + * Return the target Session of this proxy. + *

This will typically be the native provider Session + * or a wrapper from a session pool. + * @return the underlying Session (never null) + */ + Session getTargetSession(); + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java new file mode 100644 index 00000000000..921f109a6a4 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java @@ -0,0 +1,587 @@ +/* + * Copyright 2002-2008 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.connection; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.ExceptionListener; +import javax.jms.JMSException; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.Session; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; + +/** + * A JMS ConnectionFactory adapter that returns the same Connection + * from all {@link #createConnection()} calls, and ignores calls to + * {@link javax.jms.Connection#close()}. According to the JMS Connection + * model, this is perfectly thread-safe (in contrast to e.g. JDBC). The + * shared Connection can be automatically recovered in case of an Exception. + * + *

You can either pass in a specific JMS Connection directly or let this + * factory lazily create a Connection via a given target ConnectionFactory. + * This factory generally works with JMS 1.1 as well as JMS 1.0.2; use + * {@link SingleConnectionFactory102} for strict JMS 1.0.2 only usage. + * + *

Note that when using the JMS 1.0.2 API, this ConnectionFactory will switch + * into queue/topic mode according to the JMS API methods used at runtime: + * createQueueConnection and createTopicConnection will + * lead to queue/topic mode, respectively; generic createConnection + * calls will lead to a JMS 1.1 connection which is able to serve both modes. + * + *

Useful for testing and standalone environments in order to keep using the + * same Connection for multiple {@link org.springframework.jms.core.JmsTemplate} + * calls, without having a pooling ConnectionFactory underneath. This may span + * any number of transactions, even concurrently executing transactions. + * + *

Note that Spring's message listener containers support the use of + * a shared Connection within each listener container instance. Using + * SingleConnectionFactory in combination only really makes sense for + * sharing a single JMS Connection across multiple listener containers. + * + * @author Juergen Hoeller + * @author Mark Pollack + * @since 1.1 + * @see org.springframework.jms.core.JmsTemplate + * @see org.springframework.jms.listener.SimpleMessageListenerContainer + * @see org.springframework.jms.listener.DefaultMessageListenerContainer#setCacheLevel + */ +public class SingleConnectionFactory + implements ConnectionFactory, QueueConnectionFactory, TopicConnectionFactory, ExceptionListener, + InitializingBean, DisposableBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private ConnectionFactory targetConnectionFactory; + + private String clientId; + + private ExceptionListener exceptionListener; + + private boolean reconnectOnException = false; + + /** Wrapped Connection */ + private Connection target; + + /** Proxy Connection */ + private Connection connection; + + /** A hint whether to create a queue or topic connection */ + private Boolean pubSubMode; + + /** Whether the shared Connection has been started */ + private boolean started = false; + + /** Synchronization monitor for the shared Connection */ + private final Object connectionMonitor = new Object(); + + + /** + * Create a new SingleConnectionFactory for bean-style usage. + * @see #setTargetConnectionFactory + */ + public SingleConnectionFactory() { + } + + /** + * Create a new SingleConnectionFactory that always returns the given Connection. + * @param target the single Connection + */ + public SingleConnectionFactory(Connection target) { + Assert.notNull(target, "Target Connection must not be null"); + this.target = target; + this.connection = getSharedConnectionProxy(target); + } + + /** + * Create a new SingleConnectionFactory that always returns a single + * Connection that it will lazily create via the given target + * ConnectionFactory. + * @param targetConnectionFactory the target ConnectionFactory + */ + public SingleConnectionFactory(ConnectionFactory targetConnectionFactory) { + Assert.notNull(targetConnectionFactory, "Target ConnectionFactory must not be null"); + this.targetConnectionFactory = targetConnectionFactory; + } + + + /** + * Set the target ConnectionFactory which will be used to lazily + * create a single Connection. + */ + public void setTargetConnectionFactory(ConnectionFactory targetConnectionFactory) { + this.targetConnectionFactory = targetConnectionFactory; + } + + /** + * Return the target ConnectionFactory which will be used to lazily + * create a single Connection, if any. + */ + public ConnectionFactory getTargetConnectionFactory() { + return this.targetConnectionFactory; + } + + /** + * Specify a JMS client ID for the single Connection created and exposed + * by this ConnectionFactory. + *

Note that client IDs need to be unique among all active Connections + * of the underlying JMS provider. Furthermore, a client ID can only be + * assigned if the original ConnectionFactory hasn't already assigned one. + * @see javax.jms.Connection#setClientID + * @see #setTargetConnectionFactory + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * Return a JMS client ID for the single Connection created and exposed + * by this ConnectionFactory, if any. + */ + protected String getClientId() { + return this.clientId; + } + + /** + * Specify an JMS ExceptionListener implementation that should be + * registered with with the single Connection created by this factory. + * @see #setReconnectOnException + */ + public void setExceptionListener(ExceptionListener exceptionListener) { + this.exceptionListener = exceptionListener; + } + + /** + * Return the JMS ExceptionListener implementation that should be registered + * with with the single Connection created by this factory, if any. + */ + protected ExceptionListener getExceptionListener() { + return this.exceptionListener; + } + + /** + * Specify whether the single Connection should be reset (to be subsequently renewed) + * when a JMSException is reported by the underlying Connection. + *

Default is "false". Switch this to "true" to automatically trigger + * recovery based on your JMS provider's exception notifications. + *

Internally, this will lead to a special JMS ExceptionListener + * (this SingleConnectionFactory itself) being registered with the + * underlying Connection. This can also be combined with a + * user-specified ExceptionListener, if desired. + * @see #setExceptionListener + */ + public void setReconnectOnException(boolean reconnectOnException) { + this.reconnectOnException = reconnectOnException; + } + + /** + * Return whether the single Connection should be renewed when + * a JMSException is reported by the underlying Connection. + */ + protected boolean isReconnectOnException() { + return this.reconnectOnException; + } + + /** + * Make sure a Connection or ConnectionFactory has been set. + */ + public void afterPropertiesSet() { + if (this.connection == null && getTargetConnectionFactory() == null) { + throw new IllegalArgumentException("Connection or 'targetConnectionFactory' is required"); + } + } + + + public Connection createConnection() throws JMSException { + synchronized (this.connectionMonitor) { + if (this.connection == null) { + initConnection(); + } + return this.connection; + } + } + + public Connection createConnection(String username, String password) throws JMSException { + throw new javax.jms.IllegalStateException( + "SingleConnectionFactory does not support custom username and password"); + } + + public QueueConnection createQueueConnection() throws JMSException { + Connection con = null; + synchronized (this.connectionMonitor) { + this.pubSubMode = Boolean.FALSE; + con = createConnection(); + } + if (!(con instanceof QueueConnection)) { + throw new javax.jms.IllegalStateException( + "This SingleConnectionFactory does not hold a QueueConnection but rather: " + con); + } + return ((QueueConnection) con); + } + + public QueueConnection createQueueConnection(String username, String password) throws JMSException { + throw new javax.jms.IllegalStateException( + "SingleConnectionFactory does not support custom username and password"); + } + + public TopicConnection createTopicConnection() throws JMSException { + Connection con = null; + synchronized (this.connectionMonitor) { + this.pubSubMode = Boolean.TRUE; + con = createConnection(); + } + if (!(con instanceof TopicConnection)) { + throw new javax.jms.IllegalStateException( + "This SingleConnectionFactory does not hold a TopicConnection but rather: " + con); + } + return ((TopicConnection) con); + } + + public TopicConnection createTopicConnection(String username, String password) throws JMSException { + throw new javax.jms.IllegalStateException( + "SingleConnectionFactory does not support custom username and password"); + } + + + /** + * Initialize the underlying shared Connection. + *

Closes and reinitializes the Connection if an underlying + * Connection is present already. + * @throws javax.jms.JMSException if thrown by JMS API methods + */ + public void initConnection() throws JMSException { + if (getTargetConnectionFactory() == null) { + throw new IllegalStateException( + "'targetConnectionFactory' is required for lazily initializing a Connection"); + } + synchronized (this.connectionMonitor) { + if (this.target != null) { + closeConnection(this.target); + } + this.target = doCreateConnection(); + prepareConnection(this.target); + if (logger.isInfoEnabled()) { + logger.info("Established shared JMS Connection: " + this.target); + } + this.connection = getSharedConnectionProxy(this.target); + } + } + + /** + * Exception listener callback that renews the underlying single Connection. + */ + public void onException(JMSException ex) { + resetConnection(); + } + + /** + * Close the underlying shared connection. + * The provider of this ConnectionFactory needs to care for proper shutdown. + *

As this bean implements DisposableBean, a bean factory will + * automatically invoke this on destruction of its cached singletons. + */ + public void destroy() { + resetConnection(); + } + + /** + * Reset the underlying shared Connection, to be reinitialized on next access. + */ + public void resetConnection() { + synchronized (this.connectionMonitor) { + if (this.target != null) { + closeConnection(this.target); + } + this.target = null; + this.connection = null; + } + } + + /** + * Create a JMS Connection via this template's ConnectionFactory. + * @return the new JMS Connection + * @throws javax.jms.JMSException if thrown by JMS API methods + */ + protected Connection doCreateConnection() throws JMSException { + ConnectionFactory cf = getTargetConnectionFactory(); + if (Boolean.FALSE.equals(this.pubSubMode) && cf instanceof QueueConnectionFactory) { + return ((QueueConnectionFactory) cf).createQueueConnection(); + } + else if (Boolean.TRUE.equals(this.pubSubMode) && cf instanceof TopicConnectionFactory) { + return ((TopicConnectionFactory) cf).createTopicConnection(); + } + else { + return getTargetConnectionFactory().createConnection(); + } + } + + /** + * Prepare the given Connection before it is exposed. + *

The default implementation applies ExceptionListener and client id. + * Can be overridden in subclasses. + * @param con the Connection to prepare + * @throws JMSException if thrown by JMS API methods + * @see #setExceptionListener + * @see #setReconnectOnException + */ + protected void prepareConnection(Connection con) throws JMSException { + if (getClientId() != null) { + con.setClientID(getClientId()); + } + if (getExceptionListener() != null || isReconnectOnException()) { + ExceptionListener listenerToUse = getExceptionListener(); + if (isReconnectOnException()) { + listenerToUse = new InternalChainedExceptionListener(this, listenerToUse); + } + con.setExceptionListener(listenerToUse); + } + } + + /** + * Template method for obtaining a (potentially cached) Session. + *

The default implementation always returns null. + * Subclasses may override this for exposing specific Session handles, + * possibly delegating to {@link #createSession} for the creation of raw + * Session objects that will then get wrapped and returned from here. + * @param con the JMS Connection to operate on + * @param mode the Session acknowledgement mode + * (Session.TRANSACTED or one of the common modes) + * @return the Session to use, or null to indicate + * creation of a raw standard Session + * @throws JMSException if thrown by the JMS API + */ + protected Session getSession(Connection con, Integer mode) throws JMSException { + return null; + } + + /** + * Create a default Session for this ConnectionFactory, + * adaptign to JMS 1.0.2 style queue/topic mode if necessary. + * @param con the JMS Connection to operate on + * @param mode the Session acknowledgement mode + * (Session.TRANSACTED or one of the common modes) + * @return the newly created Session + * @throws JMSException if thrown by the JMS API + */ + protected Session createSession(Connection con, Integer mode) throws JMSException { + // Determine JMS API arguments... + boolean transacted = (mode.intValue() == Session.SESSION_TRANSACTED); + int ackMode = (transacted ? Session.AUTO_ACKNOWLEDGE : mode.intValue()); + // Now actually call the appropriate JMS factory method... + if (Boolean.FALSE.equals(this.pubSubMode) && con instanceof QueueConnection) { + return ((QueueConnection) con).createQueueSession(transacted, ackMode); + } + else if (Boolean.TRUE.equals(this.pubSubMode) && con instanceof TopicConnection) { + return ((TopicConnection) con).createTopicSession(transacted, ackMode); + } + else { + return con.createSession(transacted, ackMode); + } + } + + /** + * Close the given Connection. + * @param con the Connection to close + */ + protected void closeConnection(Connection con) { + if (logger.isDebugEnabled()) { + logger.debug("Closing shared JMS Connection: " + this.target); + } + try { + try { + if (this.started) { + this.started = false; + con.stop(); + } + } + finally { + con.close(); + } + } + catch (javax.jms.IllegalStateException ex) { + logger.debug("Ignoring Connection state exception - assuming already closed: " + ex); + } + catch (Throwable ex) { + logger.debug("Could not close shared JMS Connection", ex); + } + } + + /** + * Wrap the given Connection with a proxy that delegates every method call to it + * but suppresses close calls. This is useful for allowing application code to + * handle a special framework Connection just like an ordinary Connection from a + * JMS ConnectionFactory. + * @param target the original Connection to wrap + * @return the wrapped Connection + */ + protected Connection getSharedConnectionProxy(Connection target) { + List classes = new ArrayList(3); + classes.add(Connection.class); + if (target instanceof QueueConnection) { + classes.add(QueueConnection.class); + } + if (target instanceof TopicConnection) { + classes.add(TopicConnection.class); + } + return (Connection) Proxy.newProxyInstance( + Connection.class.getClassLoader(), + (Class[]) classes.toArray(new Class[classes.size()]), + new SharedConnectionInvocationHandler(target)); + } + + + /** + * Invocation handler for a cached JMS Connection proxy. + */ + private class SharedConnectionInvocationHandler implements InvocationHandler { + + private final Connection target; + + public SharedConnectionInvocationHandler(Connection target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of Connection proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("toString")) { + return "Shared JMS Connection: " + this.target; + } + else if (method.getName().equals("setClientID")) { + // Handle setClientID method: throw exception if not compatible. + String currentClientId = this.target.getClientID(); + if (currentClientId != null && currentClientId.equals(args[0])) { + return null; + } + else { + throw new javax.jms.IllegalStateException( + "setClientID call not supported on proxy for shared Connection. " + + "Set the 'clientId' property on the SingleConnectionFactory instead."); + } + } + else if (method.getName().equals("setExceptionListener")) { + // Handle setExceptionListener method: add to the chain. + ExceptionListener currentExceptionListener = this.target.getExceptionListener(); + if (currentExceptionListener instanceof InternalChainedExceptionListener && args[0] != null) { + ((InternalChainedExceptionListener) currentExceptionListener).addDelegate((ExceptionListener) args[0]); + return null; + } + else { + throw new javax.jms.IllegalStateException( + "setExceptionListener call not supported on proxy for shared Connection. " + + "Set the 'exceptionListener' property on the SingleConnectionFactory instead. " + + "Alternatively, activate SingleConnectionFactory's 'reconnectOnException' feature, " + + "which will allow for registering further ExceptionListeners to the recovery chain."); + } + } + else if (method.getName().equals("start")) { + // Handle start method: track started state. + this.target.start(); + synchronized (connectionMonitor) { + started = true; + } + return null; + } + else if (method.getName().equals("stop")) { + // Handle stop method: don't pass the call on. + return null; + } + else if (method.getName().equals("close")) { + // Handle close method: don't pass the call on. + return null; + } + else if (method.getName().equals("createSession") || method.getName().equals("createQueueSession") || + method.getName().equals("createTopicSession")) { + boolean transacted = ((Boolean) args[0]).booleanValue(); + Integer ackMode = (Integer) args[1]; + Integer mode = (transacted ? new Integer(Session.SESSION_TRANSACTED) : ackMode); + Session session = getSession(this.target, mode); + if (session != null) { + if (!method.getReturnType().isInstance(session)) { + String msg = "JMS Session does not implement specific domain: " + session; + try { + session.close(); + } + catch (Throwable ex) { + logger.trace("Failed to close newly obtained JMS Session", ex); + } + throw new javax.jms.IllegalStateException(msg); + } + return session; + } + } + try { + Object retVal = method.invoke(this.target, args); + if (method.getName().equals("getExceptionListener") && retVal instanceof InternalChainedExceptionListener) { + // Handle getExceptionListener method: hide internal chain. + InternalChainedExceptionListener listener = (InternalChainedExceptionListener) retVal; + return listener.getUserListener(); + } + else { + return retVal; + } + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + + + /** + * Internal chained ExceptionListener for handling the internal recovery listener + * in combination with a user-specified listener. + */ + private static class InternalChainedExceptionListener extends ChainedExceptionListener { + + private ExceptionListener userListener; + + public InternalChainedExceptionListener(ExceptionListener internalListener, ExceptionListener userListener) { + addDelegate(internalListener); + if (userListener != null) { + addDelegate(userListener); + this.userListener = userListener; + } + } + + public ExceptionListener getUserListener() { + return this.userListener; + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory102.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory102.java new file mode 100644 index 00000000000..4fc9c54774c --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory102.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2007 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.connection; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.QueueConnectionFactory; +import javax.jms.TopicConnectionFactory; + +/** + * A subclass of {@link SingleConnectionFactory} for the JMS 1.0.2 specification, + * not relying on JMS 1.1 methods like SingleConnectionFactory itself. + * This class can be used for JMS 1.0.2 providers, offering the same API as + * SingleConnectionFactory does for JMS 1.1 providers. + * + *

You need to set the {@link #setPubSubDomain "pubSubDomain" property}, + * since this class will always explicitly differentiate between a + * {@link javax.jms.QueueConnection} and a {@link javax.jms.TopicConnection}. + * + * @author Juergen Hoeller + * @since 1.1 + * @see #setTargetConnectionFactory + * @see #setPubSubDomain + */ +public class SingleConnectionFactory102 extends SingleConnectionFactory { + + private boolean pubSubDomain = false; + + + /** + * Create a new SingleConnectionFactory102 for bean-style usage. + */ + public SingleConnectionFactory102() { + super(); + } + + /** + * Create a new SingleConnectionFactory102 that always returns a single + * Connection that it will lazily create via the given target + * ConnectionFactory. + * @param connectionFactory the target ConnectionFactory + * @param pubSubDomain whether the Publish/Subscribe domain (Topics) or + * Point-to-Point domain (Queues) should be used + */ + public SingleConnectionFactory102(ConnectionFactory connectionFactory, boolean pubSubDomain) { + setTargetConnectionFactory(connectionFactory); + this.pubSubDomain = pubSubDomain; + afterPropertiesSet(); + } + + + /** + * Configure the factory with knowledge of the JMS domain used. + * This tells the JMS 1.0.2 provider which class hierarchy to use for creating + * Connections and Sessions. + *

Default is Point-to-Point (Queues). + * @param pubSubDomain true for Publish/Subscribe domain (Topics), + * false for Point-to-Point domain (Queues) + */ + public void setPubSubDomain(boolean pubSubDomain) { + this.pubSubDomain = pubSubDomain; + } + + /** + * Return whether the Publish/Subscribe domain (Topics) is used. + * Otherwise, the Point-to-Point domain (Queues) is used. + */ + public boolean isPubSubDomain() { + return this.pubSubDomain; + } + + + /** + * In addition to checking whether the target ConnectionFactory is set, + * make sure that the supplied factory is of the appropriate type for + * the specified destination type: QueueConnectionFactory for queues, + * TopicConnectionFactory for topics. + */ + public void afterPropertiesSet() { + super.afterPropertiesSet(); + + // Make sure that the ConnectionFactory passed is consistent. + // Some provider implementations of the ConnectionFactory interface + // implement both domain interfaces under the cover, so just check if + // the selected domain is consistent with the type of connection factory. + if (isPubSubDomain()) { + if (!(getTargetConnectionFactory() instanceof TopicConnectionFactory)) { + throw new IllegalArgumentException( + "Specified a Spring JMS 1.0.2 SingleConnectionFactory for topics " + + "but did not supply an instance of TopicConnectionFactory"); + } + } + else { + if (!(getTargetConnectionFactory() instanceof QueueConnectionFactory)) { + throw new IllegalArgumentException( + "Specified a Spring JMS 1.0.2 SingleConnectionFactory for queues " + + "but did not supply an instance of QueueConnectionFactory"); + } + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Connection doCreateConnection() throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnectionFactory) getTargetConnectionFactory()).createTopicConnection(); + } + else { + return ((QueueConnectionFactory) getTargetConnectionFactory()).createQueueConnection(); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/SmartConnectionFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/SmartConnectionFactory.java new file mode 100644 index 00000000000..27b6ec73ad7 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/SmartConnectionFactory.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2007 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.connection; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; + +/** + * Extension of the javax.jms.ConnectionFactory interface, + * indicating how to release Connections obtained from it. + * + * @author Juergen Hoeller + * @since 2.0.2 + */ +public interface SmartConnectionFactory extends ConnectionFactory { + + /** + * Should we stop the Connection, obtained from this ConnectionFactory? + * @param con the Connection to check + * @return whether a stop call is necessary + * @see javax.jms.Connection#stop() + */ + boolean shouldStop(Connection con); + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/SynchedLocalTransactionFailedException.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/SynchedLocalTransactionFailedException.java new file mode 100644 index 00000000000..16a30f3ca1e --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/SynchedLocalTransactionFailedException.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 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.connection; + +import javax.jms.JMSException; + +import org.springframework.jms.JmsException; + +/** + * Exception thrown when a synchronized local transaction failed to complete + * (after the main transaction has already completed). + * + * @author Juergen Hoeller + * @since 2.0 + * @see ConnectionFactoryUtils + */ +public class SynchedLocalTransactionFailedException extends JmsException { + + /** + * Create a new SynchedLocalTransactionFailedException. + * @param msg the detail message + * @param cause the root cause + */ + public SynchedLocalTransactionFailedException(String msg, JMSException cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/TransactionAwareConnectionFactoryProxy.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/TransactionAwareConnectionFactoryProxy.java new file mode 100644 index 00000000000..598664cbb22 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/TransactionAwareConnectionFactoryProxy.java @@ -0,0 +1,329 @@ +/* + * Copyright 2002-2008 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.connection; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; +import javax.jms.TopicSession; +import javax.jms.TransactionInProgressException; + +import org.springframework.util.Assert; + +/** + * Proxy for a target JMS {@link javax.jms.ConnectionFactory}, adding awareness of + * Spring-managed transactions. Similar to a transactional JNDI ConnectionFactory + * as provided by a J2EE server. + * + *

Messaging code that should remain unaware of Spring's JMS support can work + * with this proxy to seamlessly participate in Spring-managed transactions. + * Note that the transaction manager, for example {@link JmsTransactionManager}, + * still needs to work with the underlying ConnectionFactory, not with + * this proxy. + * + *

Make sure that TransactionAwareConnectionFactoryProxy is the outermost + * ConnectionFactory of a chain of ConnectionFactory proxies/adapters. + * TransactionAwareConnectionFactoryProxy can delegate either directly to the + * target factory or to some intermediary adapter like + * {@link UserCredentialsConnectionFactoryAdapter}. + * + *

Delegates to {@link ConnectionFactoryUtils} for automatically participating + * in thread-bound transactions, for example managed by {@link JmsTransactionManager}. + * createSession calls and close calls on returned Sessions + * will behave properly within a transaction, that is, always work on the transactional + * Session. If not within a transaction, normal ConnectionFactory behavior applies. + * + *

Note that transactional JMS Sessions will be registered on a per-Connection + * basis. To share the same JMS Session across a transaction, make sure that you + * operate on the same JMS Connection handle - either through reusing the handle + * or through configuring a {@link SingleConnectionFactory} underneath. + * + *

Returned transactional Session proxies will implement the {@link SessionProxy} + * interface to allow for access to the underlying target Session. This is only + * intended for accessing vendor-specific Session API or for testing purposes + * (e.g. to perform manual transaction control). For typical application purposes, + * simply use the standard JMS Session interface. + * + * @author Juergen Hoeller + * @since 2.0 + * @see UserCredentialsConnectionFactoryAdapter + * @see SingleConnectionFactory + */ +public class TransactionAwareConnectionFactoryProxy + implements ConnectionFactory, QueueConnectionFactory, TopicConnectionFactory { + + private boolean synchedLocalTransactionAllowed = false; + + private ConnectionFactory targetConnectionFactory; + + + /** + * Create a new TransactionAwareConnectionFactoryProxy. + */ + public TransactionAwareConnectionFactoryProxy() { + } + + /** + * Create a new TransactionAwareConnectionFactoryProxy. + * @param targetConnectionFactory the target ConnectionFactory + */ + public TransactionAwareConnectionFactoryProxy(ConnectionFactory targetConnectionFactory) { + setTargetConnectionFactory(targetConnectionFactory); + } + + + /** + * Set the target ConnectionFactory that this ConnectionFactory should delegate to. + */ + public final void setTargetConnectionFactory(ConnectionFactory targetConnectionFactory) { + Assert.notNull(targetConnectionFactory, "targetConnectionFactory must not be nul"); + this.targetConnectionFactory = targetConnectionFactory; + } + + /** + * Return the target ConnectionFactory that this ConnectionFactory should delegate to. + */ + protected ConnectionFactory getTargetConnectionFactory() { + return this.targetConnectionFactory; + } + + /** + * Set whether to allow for a local JMS transaction that is synchronized with a + * Spring-managed transaction (where the main transaction might be a JDBC-based + * one for a specific DataSource, for example), with the JMS transaction committing + * right after the main transaction. If not allowed, the given ConnectionFactory + * needs to handle transaction enlistment underneath the covers. + *

Default is "false": If not within a managed transaction that encompasses + * the underlying JMS ConnectionFactory, standard Sessions will be returned. + * Turn this flag on to allow participation in any Spring-managed transaction, + * with a local JMS transaction synchronized with the main transaction. + */ + public void setSynchedLocalTransactionAllowed(boolean synchedLocalTransactionAllowed) { + this.synchedLocalTransactionAllowed = synchedLocalTransactionAllowed; + } + + /** + * Return whether to allow for a local JMS transaction that is synchronized + * with a Spring-managed transaction. + */ + protected boolean isSynchedLocalTransactionAllowed() { + return this.synchedLocalTransactionAllowed; + } + + + public Connection createConnection() throws JMSException { + Connection targetConnection = this.targetConnectionFactory.createConnection(); + return getTransactionAwareConnectionProxy(targetConnection); + } + + public Connection createConnection(String username, String password) throws JMSException { + Connection targetConnection = this.targetConnectionFactory.createConnection(username, password); + return getTransactionAwareConnectionProxy(targetConnection); + } + + public QueueConnection createQueueConnection() throws JMSException { + if (!(this.targetConnectionFactory instanceof QueueConnectionFactory)) { + throw new javax.jms.IllegalStateException("'targetConnectionFactory' is no QueueConnectionFactory"); + } + QueueConnection targetConnection = + ((QueueConnectionFactory) this.targetConnectionFactory).createQueueConnection(); + return (QueueConnection) getTransactionAwareConnectionProxy(targetConnection); + } + + public QueueConnection createQueueConnection(String username, String password) throws JMSException { + if (!(this.targetConnectionFactory instanceof QueueConnectionFactory)) { + throw new javax.jms.IllegalStateException("'targetConnectionFactory' is no QueueConnectionFactory"); + } + QueueConnection targetConnection = + ((QueueConnectionFactory) this.targetConnectionFactory).createQueueConnection(username, password); + return (QueueConnection) getTransactionAwareConnectionProxy(targetConnection); + } + + public TopicConnection createTopicConnection() throws JMSException { + if (!(this.targetConnectionFactory instanceof TopicConnectionFactory)) { + throw new javax.jms.IllegalStateException("'targetConnectionFactory' is no TopicConnectionFactory"); + } + TopicConnection targetConnection = + ((TopicConnectionFactory) this.targetConnectionFactory).createTopicConnection(); + return (TopicConnection) getTransactionAwareConnectionProxy(targetConnection); + } + + public TopicConnection createTopicConnection(String username, String password) throws JMSException { + if (!(this.targetConnectionFactory instanceof TopicConnectionFactory)) { + throw new javax.jms.IllegalStateException("'targetConnectionFactory' is no TopicConnectionFactory"); + } + TopicConnection targetConnection = + ((TopicConnectionFactory) this.targetConnectionFactory).createTopicConnection(username, password); + return (TopicConnection) getTransactionAwareConnectionProxy(targetConnection); + } + + + /** + * Wrap the given Connection with a proxy that delegates every method call to it + * but handles Session lookup in a transaction-aware fashion. + * @param target the original Connection to wrap + * @return the wrapped Connection + */ + private Connection getTransactionAwareConnectionProxy(Connection target) { + List classes = new ArrayList(3); + classes.add(Connection.class); + if (target instanceof QueueConnection) { + classes.add(QueueConnection.class); + } + if (target instanceof TopicConnection) { + classes.add(TopicConnection.class); + } + return (Connection) Proxy.newProxyInstance( + Connection.class.getClassLoader(), + (Class[]) classes.toArray(new Class[classes.size()]), + new TransactionAwareConnectionInvocationHandler(target)); + } + + + /** + * Invocation handler that exposes transactional Sessions for the underlying Connection. + */ + private class TransactionAwareConnectionInvocationHandler implements InvocationHandler { + + private final Connection target; + + public TransactionAwareConnectionInvocationHandler(Connection target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on ConnectionProxy interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of Connection proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (Session.class.equals(method.getReturnType())) { + Session session = ConnectionFactoryUtils.getTransactionalSession( + getTargetConnectionFactory(), this.target, isSynchedLocalTransactionAllowed()); + if (session != null) { + return getCloseSuppressingSessionProxy(session); + } + } + else if (QueueSession.class.equals(method.getReturnType())) { + QueueSession session = ConnectionFactoryUtils.getTransactionalQueueSession( + (QueueConnectionFactory) getTargetConnectionFactory(), (QueueConnection) this.target, + isSynchedLocalTransactionAllowed()); + if (session != null) { + return getCloseSuppressingSessionProxy(session); + } + } + else if (TopicSession.class.equals(method.getReturnType())) { + TopicSession session = ConnectionFactoryUtils.getTransactionalTopicSession( + (TopicConnectionFactory) getTargetConnectionFactory(), (TopicConnection) this.target, + isSynchedLocalTransactionAllowed()); + if (session != null) { + return getCloseSuppressingSessionProxy(session); + } + } + + // Invoke method on target Connection. + try { + return method.invoke(this.target, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + + private Session getCloseSuppressingSessionProxy(Session target) { + List classes = new ArrayList(3); + classes.add(SessionProxy.class); + if (target instanceof QueueSession) { + classes.add(QueueSession.class); + } + if (target instanceof TopicSession) { + classes.add(TopicSession.class); + } + return (Session) Proxy.newProxyInstance( + SessionProxy.class.getClassLoader(), + (Class[]) classes.toArray(new Class[classes.size()]), + new CloseSuppressingSessionInvocationHandler(target)); + } + } + + + /** + * Invocation handler that suppresses close calls for a transactional JMS Session. + */ + private static class CloseSuppressingSessionInvocationHandler implements InvocationHandler { + + private final Session target; + + public CloseSuppressingSessionInvocationHandler(Session target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on SessionProxy interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0] ? Boolean.TRUE : Boolean.FALSE); + } + else if (method.getName().equals("hashCode")) { + // Use hashCode of Connection proxy. + return new Integer(System.identityHashCode(proxy)); + } + else if (method.getName().equals("commit")) { + throw new TransactionInProgressException("Commit call not allowed within a managed transaction"); + } + else if (method.getName().equals("rollback")) { + throw new TransactionInProgressException("Rollback call not allowed within a managed transaction"); + } + else if (method.getName().equals("close")) { + // Handle close method: not to be closed within a transaction. + return null; + } + else if (method.getName().equals("getTargetSession")) { + // Handle getTargetSession method: return underlying Session. + return this.target; + } + + // Invoke method on target Session. + try { + return method.invoke(this.target, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java b/org.springframework.jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java new file mode 100644 index 00000000000..ed65ef65da0 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/UserCredentialsConnectionFactoryAdapter.java @@ -0,0 +1,299 @@ +/* + * Copyright 2002-2008 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.connection; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.NamedThreadLocal; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * An adapter for a target JMS {@link javax.jms.ConnectionFactory}, applying the + * given user credentials to every standard createConnection() call, + * that is, implicitly invoking createConnection(username, password) + * on the target. All other methods simply delegate to the corresponding methods + * of the target ConnectionFactory. + * + *

Can be used to proxy a target JNDI ConnectionFactory that does not have user + * credentials configured. Client code can work with the ConnectionFactory without + * passing in username and password on every createConnection() call. + * + *

In the following example, client code can simply transparently work + * with the preconfigured "myConnectionFactory", implicitly accessing + * "myTargetConnectionFactory" with the specified user credentials. + * + *

+ * <bean id="myTargetConnectionFactory" class="org.springframework.jndi.JndiObjectFactoryBean">
+ *   <property name="jndiName" value="java:comp/env/jms/mycf"/>
+ * </bean>
+ *
+ * <bean id="myConnectionFactory" class="org.springframework.jms.connection.UserCredentialsConnectionFactoryAdapter">
+ *   <property name="targetConnectionFactory" ref="myTargetConnectionFactory"/>
+ *   <property name="username" value="myusername"/>
+ *   <property name="password" value="mypassword"/>
+ * </bean>
+ * + *

If the "username" is empty, this proxy will simply delegate to the standard + * createConnection() method of the target ConnectionFactory. + * This can be used to keep a UserCredentialsConnectionFactoryAdapter bean + * definition just for the option of implicitly passing in user credentials + * if the particular target ConnectionFactory requires it. + * + * @author Juergen Hoeller + * @since 1.2 + * @see #createConnection + * @see #createQueueConnection + * @see #createTopicConnection + */ +public class UserCredentialsConnectionFactoryAdapter + implements ConnectionFactory, QueueConnectionFactory, TopicConnectionFactory, InitializingBean { + + private ConnectionFactory targetConnectionFactory; + + private String username; + + private String password; + + private final ThreadLocal threadBoundCredentials = new NamedThreadLocal("Current JMS user credentials"); + + + /** + * Set the target ConnectionFactory that this ConnectionFactory should delegate to. + */ + public void setTargetConnectionFactory(ConnectionFactory targetConnectionFactory) { + Assert.notNull(targetConnectionFactory, "'targetConnectionFactory' must not be null"); + this.targetConnectionFactory = targetConnectionFactory; + } + + /** + * Set the username that this adapter should use for retrieving Connections. + * Default is no specific user. + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Set the password that this adapter should use for retrieving Connections. + * Default is no specific password. + */ + public void setPassword(String password) { + this.password = password; + } + + public void afterPropertiesSet() { + if (this.targetConnectionFactory == null) { + throw new IllegalArgumentException("Property 'targetConnectionFactory' is required"); + } + } + + + /** + * Set user credententials for this proxy and the current thread. + * The given username and password will be applied to all subsequent + * createConnection() calls on this ConnectionFactory proxy. + *

This will override any statically specified user credentials, + * that is, values of the "username" and "password" bean properties. + * @param username the username to apply + * @param password the password to apply + * @see #removeCredentialsFromCurrentThread + */ + public void setCredentialsForCurrentThread(String username, String password) { + this.threadBoundCredentials.set(new JmsUserCredentials(username, password)); + } + + /** + * Remove any user credentials for this proxy from the current thread. + * Statically specified user credentials apply again afterwards. + * @see #setCredentialsForCurrentThread + */ + public void removeCredentialsFromCurrentThread() { + this.threadBoundCredentials.set(null); + } + + + /** + * Determine whether there are currently thread-bound credentials, + * using them if available, falling back to the statically specified + * username and password (i.e. values of the bean properties) else. + * @see #doCreateConnection + */ + public final Connection createConnection() throws JMSException { + JmsUserCredentials threadCredentials = (JmsUserCredentials) this.threadBoundCredentials.get(); + if (threadCredentials != null) { + return doCreateConnection(threadCredentials.username, threadCredentials.password); + } + else { + return doCreateConnection(this.username, this.password); + } + } + + /** + * Delegate the call straight to the target ConnectionFactory. + */ + public Connection createConnection(String username, String password) throws JMSException { + return doCreateConnection(username, password); + } + + /** + * This implementation delegates to the createConnection(username, password) + * method of the target ConnectionFactory, passing in the specified user credentials. + * If the specified username is empty, it will simply delegate to the standard + * createConnection() method of the target ConnectionFactory. + * @param username the username to use + * @param password the password to use + * @return the Connection + * @see javax.jms.ConnectionFactory#createConnection(String, String) + * @see javax.jms.ConnectionFactory#createConnection() + */ + protected Connection doCreateConnection(String username, String password) throws JMSException { + Assert.state(this.targetConnectionFactory != null, "'targetConnectionFactory' is required"); + if (StringUtils.hasLength(username)) { + return this.targetConnectionFactory.createConnection(username, password); + } + else { + return this.targetConnectionFactory.createConnection(); + } + } + + + /** + * Determine whether there are currently thread-bound credentials, + * using them if available, falling back to the statically specified + * username and password (i.e. values of the bean properties) else. + * @see #doCreateQueueConnection + */ + public final QueueConnection createQueueConnection() throws JMSException { + JmsUserCredentials threadCredentials = (JmsUserCredentials) this.threadBoundCredentials.get(); + if (threadCredentials != null) { + return doCreateQueueConnection(threadCredentials.username, threadCredentials.password); + } + else { + return doCreateQueueConnection(this.username, this.password); + } + } + + /** + * Delegate the call straight to the target QueueConnectionFactory. + */ + public QueueConnection createQueueConnection(String username, String password) throws JMSException { + return doCreateQueueConnection(username, password); + } + + /** + * This implementation delegates to the createQueueConnection(username, password) + * method of the target QueueConnectionFactory, passing in the specified user credentials. + * If the specified username is empty, it will simply delegate to the standard + * createQueueConnection() method of the target ConnectionFactory. + * @param username the username to use + * @param password the password to use + * @return the Connection + * @see javax.jms.QueueConnectionFactory#createQueueConnection(String, String) + * @see javax.jms.QueueConnectionFactory#createQueueConnection() + */ + protected QueueConnection doCreateQueueConnection(String username, String password) throws JMSException { + Assert.state(this.targetConnectionFactory != null, "'targetConnectionFactory' is required"); + if (!(this.targetConnectionFactory instanceof QueueConnectionFactory)) { + throw new javax.jms.IllegalStateException("'targetConnectionFactory' is not a QueueConnectionFactory"); + } + QueueConnectionFactory queueFactory = (QueueConnectionFactory) this.targetConnectionFactory; + if (StringUtils.hasLength(username)) { + return queueFactory.createQueueConnection(username, password); + } + else { + return queueFactory.createQueueConnection(); + } + } + + + /** + * Determine whether there are currently thread-bound credentials, + * using them if available, falling back to the statically specified + * username and password (i.e. values of the bean properties) else. + * @see #doCreateTopicConnection + */ + public final TopicConnection createTopicConnection() throws JMSException { + JmsUserCredentials threadCredentials = (JmsUserCredentials) this.threadBoundCredentials.get(); + if (threadCredentials != null) { + return doCreateTopicConnection(threadCredentials.username, threadCredentials.password); + } + else { + return doCreateTopicConnection(this.username, this.password); + } + } + + /** + * Delegate the call straight to the target TopicConnectionFactory. + */ + public TopicConnection createTopicConnection(String username, String password) throws JMSException { + return doCreateTopicConnection(username, password); + } + + /** + * This implementation delegates to the createTopicConnection(username, password) + * method of the target TopicConnectionFactory, passing in the specified user credentials. + * If the specified username is empty, it will simply delegate to the standard + * createTopicConnection() method of the target ConnectionFactory. + * @param username the username to use + * @param password the password to use + * @return the Connection + * @see javax.jms.TopicConnectionFactory#createTopicConnection(String, String) + * @see javax.jms.TopicConnectionFactory#createTopicConnection() + */ + protected TopicConnection doCreateTopicConnection(String username, String password) throws JMSException { + Assert.state(this.targetConnectionFactory != null, "'targetConnectionFactory' is required"); + if (!(this.targetConnectionFactory instanceof TopicConnectionFactory)) { + throw new javax.jms.IllegalStateException("'targetConnectionFactory' is not a TopicConnectionFactory"); + } + TopicConnectionFactory queueFactory = (TopicConnectionFactory) this.targetConnectionFactory; + if (StringUtils.hasLength(username)) { + return queueFactory.createTopicConnection(username, password); + } + else { + return queueFactory.createTopicConnection(); + } + } + + + /** + * Inner class used as ThreadLocal value. + */ + private static class JmsUserCredentials { + + public final String username; + + public final String password; + + private JmsUserCredentials(String username, String password) { + this.username = username; + this.password = password; + } + + public String toString() { + return "JmsUserCredentials[username='" + this.username + "',password='" + this.password + "']"; + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/connection/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/connection/package.html new file mode 100644 index 00000000000..0ace47f2e50 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/connection/package.html @@ -0,0 +1,8 @@ + + + +Provides a PlatformTransactionManager implementation for a single +JMS ConnectionFactory, and a SingleConnectionFactory adapter. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/BrowserCallback.java b/org.springframework.jms/src/main/java/org/springframework/jms/core/BrowserCallback.java new file mode 100644 index 00000000000..88ab1674393 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/BrowserCallback.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2008 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.core; + +import javax.jms.JMSException; +import javax.jms.QueueBrowser; +import javax.jms.Session; + +/** + * Callback for browsing the messages in a JMS queue. + * + *

To be used with JmsTemplate's callback methods that take a BrowserCallback + * argument, often implemented as an anonymous inner class. + * + * @author Juergen Hoeller + * @since 2.5.1 + * @see JmsTemplate#browse(BrowserCallback) + * @see JmsTemplate#browseSelected(String, BrowserCallback) + */ +public interface BrowserCallback { + + /** + * Perform operations on the given {@link javax.jms.Session} and {@link javax.jms.QueueBrowser}. + *

The message producer is not associated with any destination. + * @param session the JMS Session object to use + * @param browser the JMS QueueBrowser object to use + * @return a result object from working with the Session, if any (can be null) + * @throws javax.jms.JMSException if thrown by JMS API methods + */ + Object doInJms(Session session, QueueBrowser browser) throws JMSException; + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/JmsOperations.java b/org.springframework.jms/src/main/java/org/springframework/jms/core/JmsOperations.java new file mode 100644 index 00000000000..e01d371b0e6 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/JmsOperations.java @@ -0,0 +1,425 @@ +/* + * Copyright 2002-2008 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.core; + +import javax.jms.Destination; +import javax.jms.Message; +import javax.jms.Queue; + +import org.springframework.jms.JmsException; + +/** + * Specifies a basic set of JMS operations. + * + *

Implemented by {@link JmsTemplate}. Not often used but a useful option + * to enhance testability, as it can easily be mocked or stubbed. + * + *

Provides JmsTemplate's send(..) and + * receive(..) methods that mirror various JMS API methods. + * See the JMS specification and javadocs for details on those methods. + * + * @author Mark Pollack + * @author Juergen Hoeller + * @since 1.1 + * @see JmsTemplate + * @see javax.jms.Destination + * @see javax.jms.Session + * @see javax.jms.MessageProducer + * @see javax.jms.MessageConsumer + */ +public interface JmsOperations { + + /** + * Execute the action specified by the given action object within a JMS Session. + *

When used with a 1.0.2 provider, you may need to downcast + * to the appropriate domain implementation, either QueueSession or + * TopicSession in the action objects doInJms callback method. + * @param action callback object that exposes the session + * @return the result object from working with the session + * @throws JmsException if there is any problem + */ + Object execute(SessionCallback action) throws JmsException; + + /** + * Send messages to the default JMS destination (or one specified + * for each send operation). The callback gives access to the JMS Session + * and MessageProducer in order to perform complex send operations. + * @param action callback object that exposes the session/producer pair + * @return the result object from working with the session + * @throws JmsException checked JMSException converted to unchecked + */ + Object execute(ProducerCallback action) throws JmsException; + + /** + * Send messages to a JMS destination. The callback gives access to the JMS Session + * and MessageProducer in order to perform complex send operations. + * @param destination the destination to send messages to + * @param action callback object that exposes the session/producer pair + * @return the result object from working with the session + * @throws JmsException checked JMSException converted to unchecked + */ + Object execute(Destination destination, ProducerCallback action) throws JmsException; + + /** + * Send messages to a JMS destination. The callback gives access to the JMS Session + * and MessageProducer in order to perform complex send operations. + * @param destinationName the name of the destination to send messages to + * (to be resolved to an actual destination by a DestinationResolver) + * @param action callback object that exposes the session/producer pair + * @return the result object from working with the session + * @throws JmsException checked JMSException converted to unchecked + */ + Object execute(String destinationName, ProducerCallback action) throws JmsException; + + + //------------------------------------------------------------------------- + // Convenience methods for sending messages + //------------------------------------------------------------------------- + + /** + * Send a message to the default destination. + *

This will only work with a default destination specified! + * @param messageCreator callback to create a message + * @throws JmsException checked JMSException converted to unchecked + */ + void send(MessageCreator messageCreator) throws JmsException; + + /** + * Send a message to the specified destination. + * The MessageCreator callback creates the message given a Session. + * @param destination the destination to send this message to + * @param messageCreator callback to create a message + * @throws JmsException checked JMSException converted to unchecked + */ + void send(Destination destination, MessageCreator messageCreator) throws JmsException; + + /** + * Send a message to the specified destination. + * The MessageCreator callback creates the message given a Session. + * @param destinationName the name of the destination to send this message to + * (to be resolved to an actual destination by a DestinationResolver) + * @param messageCreator callback to create a message + * @throws JmsException checked JMSException converted to unchecked + */ + void send(String destinationName, MessageCreator messageCreator) throws JmsException; + + + //------------------------------------------------------------------------- + // Convenience methods for sending auto-converted messages + //------------------------------------------------------------------------- + + /** + * Send the given object to the default destination, converting the object + * to a JMS message with a configured MessageConverter. + *

This will only work with a default destination specified! + * @param message the object to convert to a message + * @throws JmsException converted checked JMSException to unchecked + */ + void convertAndSend(Object message) throws JmsException; + + /** + * Send the given object to the specified destination, converting the object + * to a JMS message with a configured MessageConverter. + * @param destination the destination to send this message to + * @param message the object to convert to a message + * @throws JmsException converted checked JMSException to unchecked + */ + void convertAndSend(Destination destination, Object message) throws JmsException; + + /** + * Send the given object to the specified destination, converting the object + * to a JMS message with a configured MessageConverter. + * @param destinationName the name of the destination to send this message to + * (to be resolved to an actual destination by a DestinationResolver) + * @param message the object to convert to a message + * @throws JmsException checked JMSException converted to unchecked + */ + void convertAndSend(String destinationName, Object message) throws JmsException; + + /** + * Send the given object to the default destination, converting the object + * to a JMS message with a configured MessageConverter. The MessagePostProcessor + * callback allows for modification of the message after conversion. + *

This will only work with a default destination specified! + * @param message the object to convert to a message + * @param postProcessor the callback to modify the message + * @throws JmsException checked JMSException converted to unchecked + */ + void convertAndSend(Object message, MessagePostProcessor postProcessor) + throws JmsException; + + /** + * Send the given object to the specified destination, converting the object + * to a JMS message with a configured MessageConverter. The MessagePostProcessor + * callback allows for modification of the message after conversion. + * @param destination the destination to send this message to + * @param message the object to convert to a message + * @param postProcessor the callback to modify the message + * @throws JmsException checked JMSException converted to unchecked + */ + void convertAndSend(Destination destination, Object message, MessagePostProcessor postProcessor) + throws JmsException; + + /** + * Send the given object to the specified destination, converting the object + * to a JMS message with a configured MessageConverter. The MessagePostProcessor + * callback allows for modification of the message after conversion. + * @param destinationName the name of the destination to send this message to + * (to be resolved to an actual destination by a DestinationResolver) + * @param message the object to convert to a message. + * @param postProcessor the callback to modify the message + * @throws JmsException checked JMSException converted to unchecked + */ + void convertAndSend(String destinationName, Object message, MessagePostProcessor postProcessor) + throws JmsException; + + + //------------------------------------------------------------------------- + // Convenience methods for receiving messages + //------------------------------------------------------------------------- + + /** + * Receive a message synchronously from the default destination, but only + * wait up to a specified time for delivery. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + *

This will only work with a default destination specified! + * @return the message received by the consumer, or null if the timeout expires + * @throws JmsException checked JMSException converted to unchecked + */ + Message receive() throws JmsException; + + /** + * Receive a message synchronously from the specified destination, but only + * wait up to a specified time for delivery. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + * @param destination the destination to receive a message from + * @return the message received by the consumer, or null if the timeout expires + * @throws JmsException checked JMSException converted to unchecked + */ + Message receive(Destination destination) throws JmsException; + + /** + * Receive a message synchronously from the specified destination, but only + * wait up to a specified time for delivery. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + * @param destinationName the name of the destination to send this message to + * (to be resolved to an actual destination by a DestinationResolver) + * @return the message received by the consumer, or null if the timeout expires + * @throws JmsException checked JMSException converted to unchecked + */ + Message receive(String destinationName) throws JmsException; + + /** + * Receive a message synchronously from the default destination, but only + * wait up to a specified time for delivery. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + *

This will only work with a default destination specified! + * @param messageSelector the JMS message selector expression (or null if none). + * See the JMS specification for a detailed definition of selector expressions. + * @return the message received by the consumer, or null if the timeout expires + * @throws JmsException checked JMSException converted to unchecked + */ + Message receiveSelected(String messageSelector) throws JmsException; + + /** + * Receive a message synchronously from the specified destination, but only + * wait up to a specified time for delivery. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + * @param destination the destination to receive a message from + * @param messageSelector the JMS message selector expression (or null if none). + * See the JMS specification for a detailed definition of selector expressions. + * @return the message received by the consumer, or null if the timeout expires + * @throws JmsException checked JMSException converted to unchecked + */ + Message receiveSelected(Destination destination, String messageSelector) throws JmsException; + + /** + * Receive a message synchronously from the specified destination, but only + * wait up to a specified time for delivery. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + * @param destinationName the name of the destination to send this message to + * (to be resolved to an actual destination by a DestinationResolver) + * @param messageSelector the JMS message selector expression (or null if none). + * See the JMS specification for a detailed definition of selector expressions. + * @return the message received by the consumer, or null if the timeout expires + * @throws JmsException checked JMSException converted to unchecked + */ + Message receiveSelected(String destinationName, String messageSelector) throws JmsException; + + + //------------------------------------------------------------------------- + // Convenience methods for receiving auto-converted messages + //------------------------------------------------------------------------- + + /** + * Receive a message synchronously from the default destination, but only + * wait up to a specified time for delivery. Convert the message into an + * object with a configured MessageConverter. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + *

This will only work with a default destination specified! + * @return the message produced for the consumer or null if the timeout expires. + * @throws JmsException checked JMSException converted to unchecked + */ + Object receiveAndConvert() throws JmsException; + + /** + * Receive a message synchronously from the specified destination, but only + * wait up to a specified time for delivery. Convert the message into an + * object with a configured MessageConverter. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + * @param destination the destination to receive a message from + * @return the message produced for the consumer or null if the timeout expires. + * @throws JmsException checked JMSException converted to unchecked + */ + Object receiveAndConvert(Destination destination) throws JmsException; + + /** + * Receive a message synchronously from the specified destination, but only + * wait up to a specified time for delivery. Convert the message into an + * object with a configured MessageConverter. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + * @param destinationName the name of the destination to send this message to + * (to be resolved to an actual destination by a DestinationResolver) + * @return the message produced for the consumer or null if the timeout expires. + * @throws JmsException checked JMSException converted to unchecked + */ + Object receiveAndConvert(String destinationName) throws JmsException; + + /** + * Receive a message synchronously from the default destination, but only + * wait up to a specified time for delivery. Convert the message into an + * object with a configured MessageConverter. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + *

This will only work with a default destination specified! + * @param messageSelector the JMS message selector expression (or null if none). + * See the JMS specification for a detailed definition of selector expressions. + * @return the message produced for the consumer or null if the timeout expires. + * @throws JmsException checked JMSException converted to unchecked + */ + Object receiveSelectedAndConvert(String messageSelector) throws JmsException; + + /** + * Receive a message synchronously from the specified destination, but only + * wait up to a specified time for delivery. Convert the message into an + * object with a configured MessageConverter. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + * @param destination the destination to receive a message from + * @param messageSelector the JMS message selector expression (or null if none). + * See the JMS specification for a detailed definition of selector expressions. + * @return the message produced for the consumer or null if the timeout expires. + * @throws JmsException checked JMSException converted to unchecked + */ + Object receiveSelectedAndConvert(Destination destination, String messageSelector) throws JmsException; + + /** + * Receive a message synchronously from the specified destination, but only + * wait up to a specified time for delivery. Convert the message into an + * object with a configured MessageConverter. + *

This method should be used carefully, since it will block the thread + * until the message becomes available or until the timeout value is exceeded. + * @param destinationName the name of the destination to send this message to + * (to be resolved to an actual destination by a DestinationResolver) + * @param messageSelector the JMS message selector expression (or null if none). + * See the JMS specification for a detailed definition of selector expressions. + * @return the message produced for the consumer or null if the timeout expires. + * @throws JmsException checked JMSException converted to unchecked + */ + Object receiveSelectedAndConvert(String destinationName, String messageSelector) throws JmsException; + + + //------------------------------------------------------------------------- + // Convenience methods for browsing messages + //------------------------------------------------------------------------- + + /** + * Browse messages in the default JMS queue. The callback gives access to the JMS + * Session and QueueBrowser in order to browse the queue and react to the contents. + * @param action callback object that exposes the session/browser pair + * @return the result object from working with the session + * @throws JmsException checked JMSException converted to unchecked + */ + Object browse(BrowserCallback action) throws JmsException; + + /** + * Browse messages in a JMS queue. The callback gives access to the JMS Session + * and QueueBrowser in order to browse the queue and react to the contents. + * @param queue the queue to browse + * @param action callback object that exposes the session/browser pair + * @return the result object from working with the session + * @throws JmsException checked JMSException converted to unchecked + */ + Object browse(Queue queue, BrowserCallback action) throws JmsException; + + /** + * Browse messages in a JMS queue. The callback gives access to the JMS Session + * and QueueBrowser in order to browse the queue and react to the contents. + * @param queueName the name of the queue to browse + * (to be resolved to an actual destination by a DestinationResolver) + * @param action callback object that exposes the session/browser pair + * @return the result object from working with the session + * @throws JmsException checked JMSException converted to unchecked + */ + Object browse(String queueName, BrowserCallback action) throws JmsException; + + /** + * Browse selected messages in a JMS queue. The callback gives access to the JMS + * Session and QueueBrowser in order to browse the queue and react to the contents. + * @param messageSelector the JMS message selector expression (or null if none). + * See the JMS specification for a detailed definition of selector expressions. + * @param action callback object that exposes the session/browser pair + * @return the result object from working with the session + * @throws JmsException checked JMSException converted to unchecked + */ + Object browseSelected(String messageSelector, BrowserCallback action) throws JmsException; + + /** + * Browse selected messages in a JMS queue. The callback gives access to the JMS + * Session and QueueBrowser in order to browse the queue and react to the contents. + * @param queue the queue to browse + * @param messageSelector the JMS message selector expression (or null if none). + * See the JMS specification for a detailed definition of selector expressions. + * @param action callback object that exposes the session/browser pair + * @return the result object from working with the session + * @throws JmsException checked JMSException converted to unchecked + */ + Object browseSelected(Queue queue, String messageSelector, BrowserCallback action) throws JmsException; + + /** + * Browse selected messages in a JMS queue. The callback gives access to the JMS + * Session and QueueBrowser in order to browse the queue and react to the contents. + * @param queueName the name of the queue to browse + * (to be resolved to an actual destination by a DestinationResolver) + * @param messageSelector the JMS message selector expression (or null if none). + * See the JMS specification for a detailed definition of selector expressions. + * @param action callback object that exposes the session/browser pair + * @return the result object from working with the session + * @throws JmsException checked JMSException converted to unchecked + */ + Object browseSelected(String queueName, String messageSelector, BrowserCallback action) throws JmsException; + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/JmsTemplate.java b/org.springframework.jms/src/main/java/org/springframework/jms/core/JmsTemplate.java new file mode 100644 index 00000000000..bd2468fa901 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/JmsTemplate.java @@ -0,0 +1,1040 @@ +/* + * Copyright 2002-2008 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.core; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.DeliveryMode; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.QueueBrowser; +import javax.jms.Session; + +import org.springframework.jms.JmsException; +import org.springframework.jms.connection.ConnectionFactoryUtils; +import org.springframework.jms.connection.JmsResourceHolder; +import org.springframework.jms.support.JmsUtils; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.jms.support.converter.SimpleMessageConverter; +import org.springframework.jms.support.destination.JmsDestinationAccessor; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Helper class that simplifies synchronous JMS access code. + * + *

NOTE: This class requires a JMS 1.1+ provider because it builds + * on the domain-independent API. Use the {@link JmsTemplate102} subclass + * for a JMS 1.0.2 provider, e.g. when running on a J2EE 1.3 server. + * + *

If you want to use dynamic destination creation, you must specify + * the type of JMS destination to create, using the "pubSubDomain" property. + * For other operations, this is not necessary, in contrast to when working + * with JmsTemplate102. Point-to-Point (Queues) is the default domain. + * + *

Default settings for JMS Sessions are "not transacted" and "auto-acknowledge". + * As defined by the J2EE specification, the transaction and acknowledgement + * parameters are ignored when a JMS Session is created inside an active + * transaction, no matter if a JTA transaction or a Spring-managed transaction. + * To configure them for native JMS usage, specify appropriate values for + * the "sessionTransacted" and "sessionAcknowledgeMode" bean properties. + * + *

This template uses a + * {@link org.springframework.jms.support.destination.DynamicDestinationResolver} + * and a {@link org.springframework.jms.support.converter.SimpleMessageConverter} + * as default strategies for resolving a destination name or converting a message, + * respectively. These defaults can be overridden through the "destinationResolver" + * and "messageConverter" bean properties. + * + *

NOTE: The ConnectionFactory used with this template should + * return pooled Connections (or a single shared Connection) as well as pooled + * Sessions and MessageProducers. Otherwise, performance of ad-hoc JMS operations + * is going to suffer. The simplest option is to use the Spring-provided + * {@link org.springframework.jms.connection.SingleConnectionFactory} as a + * decorator for your target ConnectionFactory, reusing a single + * JMS Connection in a thread-safe fashion; this is often good enough for the + * purpose of sending messages via this template. In a J2EE environment, + * make sure that the ConnectionFactory is obtained from the + * application's environment naming context via JNDI; application servers + * typically expose pooled, transaction-aware factories there. + * + * @author Mark Pollack + * @author Juergen Hoeller + * @since 1.1 + * @see #setConnectionFactory + * @see #setPubSubDomain + * @see #setDestinationResolver + * @see #setMessageConverter + * @see JmsTemplate102 + * @see javax.jms.MessageProducer + * @see javax.jms.MessageConsumer + */ +public class JmsTemplate extends JmsDestinationAccessor implements JmsOperations { + + /** + * Timeout value indicating that a receive operation should + * check if a message is immediately available without blocking. + */ + public static final long RECEIVE_TIMEOUT_NO_WAIT = -1; + + /** + * Timeout value indicating a blocking receive without timeout. + */ + public static final long RECEIVE_TIMEOUT_INDEFINITE_WAIT = 0; + + + /** Internal ResourceFactory adapter for interacting with ConnectionFactoryUtils */ + private final JmsTemplateResourceFactory transactionalResourceFactory = new JmsTemplateResourceFactory(); + + + private Object defaultDestination; + + private MessageConverter messageConverter; + + + private boolean messageIdEnabled = true; + + private boolean messageTimestampEnabled = true; + + private boolean pubSubNoLocal = false; + + private long receiveTimeout = RECEIVE_TIMEOUT_INDEFINITE_WAIT; + + + private boolean explicitQosEnabled = false; + + private int deliveryMode = Message.DEFAULT_DELIVERY_MODE; + + private int priority = Message.DEFAULT_PRIORITY; + + private long timeToLive = Message.DEFAULT_TIME_TO_LIVE; + + + /** + * Create a new JmsTemplate for bean-style usage. + *

Note: The ConnectionFactory has to be set before using the instance. + * This constructor can be used to prepare a JmsTemplate via a BeanFactory, + * typically setting the ConnectionFactory via setConnectionFactory. + * @see #setConnectionFactory + */ + public JmsTemplate() { + initDefaultStrategies(); + } + + /** + * Create a new JmsTemplate, given a ConnectionFactory. + * @param connectionFactory the ConnectionFactory to obtain Connections from + */ + public JmsTemplate(ConnectionFactory connectionFactory) { + this(); + setConnectionFactory(connectionFactory); + afterPropertiesSet(); + } + + /** + * Initialize the default implementations for the template's strategies: + * DynamicDestinationResolver and SimpleMessageConverter. + * @see #setDestinationResolver + * @see #setMessageConverter + * @see org.springframework.jms.support.destination.DynamicDestinationResolver + * @see org.springframework.jms.support.converter.SimpleMessageConverter + */ + protected void initDefaultStrategies() { + setMessageConverter(new SimpleMessageConverter()); + } + + + /** + * Set the destination to be used on send/receive operations that do not + * have a destination parameter. + *

Alternatively, specify a "defaultDestinationName", to be + * dynamically resolved via the DestinationResolver. + * @see #send(MessageCreator) + * @see #convertAndSend(Object) + * @see #convertAndSend(Object, MessagePostProcessor) + * @see #setDefaultDestinationName(String) + */ + public void setDefaultDestination(Destination destination) { + this.defaultDestination = destination; + } + + /** + * Return the destination to be used on send/receive operations that do not + * have a destination parameter. + */ + public Destination getDefaultDestination() { + return (this.defaultDestination instanceof Destination ? (Destination) this.defaultDestination : null); + } + + private Queue getDefaultQueue() { + Destination defaultDestination = getDefaultDestination(); + if (defaultDestination != null && !(defaultDestination instanceof Queue)) { + throw new IllegalStateException( + "'defaultDestination' does not correspond to a Queue. Check configuration of JmsTemplate."); + } + return (Queue) defaultDestination; + } + + /** + * Set the destination name to be used on send/receive operations that + * do not have a destination parameter. The specified name will be + * dynamically resolved via the DestinationResolver. + *

Alternatively, specify a JMS Destination object as "defaultDestination". + * @see #send(MessageCreator) + * @see #convertAndSend(Object) + * @see #convertAndSend(Object, MessagePostProcessor) + * @see #setDestinationResolver + * @see #setDefaultDestination(javax.jms.Destination) + */ + public void setDefaultDestinationName(String destinationName) { + this.defaultDestination = destinationName; + } + + /** + * Return the destination name to be used on send/receive operations that + * do not have a destination parameter. + */ + public String getDefaultDestinationName() { + return (this.defaultDestination instanceof String ? (String) this.defaultDestination : null); + } + + private String getRequiredDefaultDestinationName() throws IllegalStateException { + String name = getDefaultDestinationName(); + if (name == null) { + throw new IllegalStateException( + "No 'defaultDestination' or 'defaultDestinationName' specified. Check configuration of JmsTemplate."); + } + return name; + } + + /** + * Set the message converter for this template. Used to resolve + * Object parameters to convertAndSend methods and Object results + * from receiveAndConvert methods. + *

The default converter is a SimpleMessageConverter, which is able + * to handle BytesMessages, TextMessages and ObjectMessages. + * @see #convertAndSend + * @see #receiveAndConvert + * @see org.springframework.jms.support.converter.SimpleMessageConverter + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = messageConverter; + } + + /** + * Return the message converter for this template. + */ + public MessageConverter getMessageConverter() { + return this.messageConverter; + } + + private MessageConverter getRequiredMessageConverter() throws IllegalStateException { + MessageConverter converter = getMessageConverter(); + if (converter == null) { + throw new IllegalStateException("No 'messageConverter' specified. Check configuration of JmsTemplate."); + } + return converter; + } + + + /** + * Set whether message IDs are enabled. Default is "true". + *

This is only a hint to the JMS producer. + * See the JMS javadocs for details. + * @see javax.jms.MessageProducer#setDisableMessageID + */ + public void setMessageIdEnabled(boolean messageIdEnabled) { + this.messageIdEnabled = messageIdEnabled; + } + + /** + * Return whether message IDs are enabled. + */ + public boolean isMessageIdEnabled() { + return this.messageIdEnabled; + } + + /** + * Set whether message timestamps are enabled. Default is "true". + *

This is only a hint to the JMS producer. + * See the JMS javadocs for details. + * @see javax.jms.MessageProducer#setDisableMessageTimestamp + */ + public void setMessageTimestampEnabled(boolean messageTimestampEnabled) { + this.messageTimestampEnabled = messageTimestampEnabled; + } + + /** + * Return whether message timestamps are enabled. + */ + public boolean isMessageTimestampEnabled() { + return this.messageTimestampEnabled; + } + + /** + * Set whether to inhibit the delivery of messages published by its own connection. + * Default is "false". + * @see javax.jms.TopicSession#createSubscriber(javax.jms.Topic, String, boolean) + */ + public void setPubSubNoLocal(boolean pubSubNoLocal) { + this.pubSubNoLocal = pubSubNoLocal; + } + + /** + * Return whether to inhibit the delivery of messages published by its own connection. + */ + public boolean isPubSubNoLocal() { + return this.pubSubNoLocal; + } + + /** + * Set the timeout to use for receive calls (in milliseconds). + *

The default is {@link #RECEIVE_TIMEOUT_INDEFINITE_WAIT}, which indicates + * a blocking receive without timeout. + *

Specify {@link #RECEIVE_TIMEOUT_NO_WAIT} to inidicate that a receive operation + * should check if a message is immediately available without blocking. + * @see javax.jms.MessageConsumer#receive(long) + * @see javax.jms.MessageConsumer#receive() + * @see javax.jms.MessageConsumer#receiveNoWait() + */ + public void setReceiveTimeout(long receiveTimeout) { + this.receiveTimeout = receiveTimeout; + } + + /** + * Return the timeout to use for receive calls (in milliseconds). + */ + public long getReceiveTimeout() { + return this.receiveTimeout; + } + + + /** + * Set if the QOS values (deliveryMode, priority, timeToLive) + * should be used for sending a message. + * @see #setDeliveryMode + * @see #setPriority + * @see #setTimeToLive + */ + public void setExplicitQosEnabled(boolean explicitQosEnabled) { + this.explicitQosEnabled = explicitQosEnabled; + } + + /** + * If "true", then the values of deliveryMode, priority, and timeToLive + * will be used when sending a message. Otherwise, the default values, + * that may be set administratively, will be used. + * @return true if overriding default values of QOS parameters + * (deliveryMode, priority, and timeToLive) + * @see #setDeliveryMode + * @see #setPriority + * @see #setTimeToLive + */ + public boolean isExplicitQosEnabled() { + return this.explicitQosEnabled; + } + + /** + * Set whether message delivery should be persistent or non-persistent, + * specified as boolean value ("true" or "false"). This will set the delivery + * mode accordingly, to either "PERSISTENT" (1) or "NON_PERSISTENT" (2). + *

Default it "true" aka delivery mode "PERSISTENT". + * @see #setDeliveryMode(int) + * @see javax.jms.DeliveryMode#PERSISTENT + * @see javax.jms.DeliveryMode#NON_PERSISTENT + */ + public void setDeliveryPersistent(boolean deliveryPersistent) { + this.deliveryMode = (deliveryPersistent ? DeliveryMode.PERSISTENT : DeliveryMode.NON_PERSISTENT); + } + + /** + * Set the delivery mode to use when sending a message. + * Default is the Message default: "PERSISTENT". + *

Since a default value may be defined administratively, + * this is only used when "isExplicitQosEnabled" equals "true". + * @param deliveryMode the delivery mode to use + * @see #isExplicitQosEnabled + * @see javax.jms.DeliveryMode#PERSISTENT + * @see javax.jms.DeliveryMode#NON_PERSISTENT + * @see javax.jms.Message#DEFAULT_DELIVERY_MODE + * @see javax.jms.MessageProducer#send(javax.jms.Message, int, int, long) + */ + public void setDeliveryMode(int deliveryMode) { + this.deliveryMode = deliveryMode; + } + + /** + * Return the delivery mode to use when sending a message. + */ + public int getDeliveryMode() { + return this.deliveryMode; + } + + /** + * Set the priority of a message when sending. + *

Since a default value may be defined administratively, + * this is only used when "isExplicitQosEnabled" equals "true". + * @see #isExplicitQosEnabled + * @see javax.jms.Message#DEFAULT_PRIORITY + * @see javax.jms.MessageProducer#send(javax.jms.Message, int, int, long) + */ + public void setPriority(int priority) { + this.priority = priority; + } + + /** + * Return the priority of a message when sending. + */ + public int getPriority() { + return this.priority; + } + + /** + * Set the time-to-live of the message when sending. + *

Since a default value may be defined administratively, + * this is only used when "isExplicitQosEnabled" equals "true". + * @param timeToLive the message's lifetime (in milliseconds) + * @see #isExplicitQosEnabled + * @see javax.jms.Message#DEFAULT_TIME_TO_LIVE + * @see javax.jms.MessageProducer#send(javax.jms.Message, int, int, long) + */ + public void setTimeToLive(long timeToLive) { + this.timeToLive = timeToLive; + } + + /** + * Return the time-to-live of the message when sending. + */ + public long getTimeToLive() { + return this.timeToLive; + } + + + //------------------------------------------------------------------------- + // JmsOperations execute methods + //------------------------------------------------------------------------- + + public Object execute(SessionCallback action) throws JmsException { + return execute(action, false); + } + + /** + * Execute the action specified by the given action object within a + * JMS Session. Generalized version of execute(SessionCallback), + * allowing the JMS Connection to be started on the fly. + *

Use execute(SessionCallback) for the general case. + * Starting the JMS Connection is just necessary for receiving messages, + * which is preferably achieved through the receive methods. + * @param action callback object that exposes the Session + * @param startConnection whether to start the Connection + * @return the result object from working with the Session + * @throws JmsException if there is any problem + * @see #execute(SessionCallback) + * @see #receive + */ + public Object execute(SessionCallback action, boolean startConnection) throws JmsException { + Assert.notNull(action, "Callback object must not be null"); + Connection conToClose = null; + Session sessionToClose = null; + try { + Session sessionToUse = ConnectionFactoryUtils.doGetTransactionalSession( + getConnectionFactory(), this.transactionalResourceFactory, startConnection); + if (sessionToUse == null) { + conToClose = createConnection(); + sessionToClose = createSession(conToClose); + if (startConnection) { + conToClose.start(); + } + sessionToUse = sessionToClose; + } + if (logger.isDebugEnabled()) { + logger.debug("Executing callback on JMS Session: " + sessionToUse); + } + return action.doInJms(sessionToUse); + } + catch (JMSException ex) { + throw convertJmsAccessException(ex); + } + finally { + JmsUtils.closeSession(sessionToClose); + ConnectionFactoryUtils.releaseConnection(conToClose, getConnectionFactory(), startConnection); + } + } + + public Object execute(ProducerCallback action) throws JmsException { + String defaultDestinationName = getDefaultDestinationName(); + if (defaultDestinationName != null) { + return execute(defaultDestinationName, action); + } + else { + return execute(getDefaultDestination(), action); + } + } + + public Object execute(final Destination destination, final ProducerCallback action) throws JmsException { + Assert.notNull(action, "Callback object must not be null"); + return execute(new SessionCallback() { + public Object doInJms(Session session) throws JMSException { + MessageProducer producer = createProducer(session, destination); + try { + return action.doInJms(session, producer); + } + finally { + JmsUtils.closeMessageProducer(producer); + } + } + }, false); + } + + public Object execute(final String destinationName, final ProducerCallback action) throws JmsException { + Assert.notNull(action, "Callback object must not be null"); + return execute(new SessionCallback() { + public Object doInJms(Session session) throws JMSException { + Destination destination = resolveDestinationName(session, destinationName); + MessageProducer producer = createProducer(session, destination); + try { + return action.doInJms(session, producer); + } + finally { + JmsUtils.closeMessageProducer(producer); + } + } + }, false); + } + + + //------------------------------------------------------------------------- + // Convenience methods for sending messages + //------------------------------------------------------------------------- + + public void send(MessageCreator messageCreator) throws JmsException { + Destination defaultDestination = getDefaultDestination(); + if (defaultDestination != null) { + send(defaultDestination, messageCreator); + } + else { + send(getRequiredDefaultDestinationName(), messageCreator); + } + } + + public void send(final Destination destination, final MessageCreator messageCreator) throws JmsException { + execute(new SessionCallback() { + public Object doInJms(Session session) throws JMSException { + doSend(session, destination, messageCreator); + return null; + } + }, false); + } + + public void send(final String destinationName, final MessageCreator messageCreator) throws JmsException { + execute(new SessionCallback() { + public Object doInJms(Session session) throws JMSException { + Destination destination = resolveDestinationName(session, destinationName); + doSend(session, destination, messageCreator); + return null; + } + }, false); + } + + /** + * Send the given JMS message. + * @param session the JMS Session to operate on + * @param destination the JMS Destination to send to + * @param messageCreator callback to create a JMS Message + * @throws JMSException if thrown by JMS API methods + */ + protected void doSend(Session session, Destination destination, MessageCreator messageCreator) + throws JMSException { + + Assert.notNull(messageCreator, "MessageCreator must not be null"); + MessageProducer producer = createProducer(session, destination); + try { + Message message = messageCreator.createMessage(session); + if (logger.isDebugEnabled()) { + logger.debug("Sending created message: " + message); + } + doSend(producer, message); + // Check commit - avoid commit call within a JTA transaction. + if (session.getTransacted() && isSessionLocallyTransacted(session)) { + // Transacted session created by this template -> commit. + JmsUtils.commitIfNecessary(session); + } + } + finally { + JmsUtils.closeMessageProducer(producer); + } + } + + /** + * Actually send the given JMS message. + * @param producer the JMS MessageProducer to send with + * @param message the JMS Message to send + * @throws JMSException if thrown by JMS API methods + */ + protected void doSend(MessageProducer producer, Message message) throws JMSException { + if (isExplicitQosEnabled()) { + producer.send(message, getDeliveryMode(), getPriority(), getTimeToLive()); + } + else { + producer.send(message); + } + } + + + //------------------------------------------------------------------------- + // Convenience methods for sending auto-converted messages + //------------------------------------------------------------------------- + + public void convertAndSend(Object message) throws JmsException { + Destination defaultDestination = getDefaultDestination(); + if (defaultDestination != null) { + convertAndSend(defaultDestination, message); + } + else { + convertAndSend(getRequiredDefaultDestinationName(), message); + } + } + + public void convertAndSend(Destination destination, final Object message) throws JmsException { + send(destination, new MessageCreator() { + public Message createMessage(Session session) throws JMSException { + return getRequiredMessageConverter().toMessage(message, session); + } + }); + } + + public void convertAndSend(String destinationName, final Object message) throws JmsException { + send(destinationName, new MessageCreator() { + public Message createMessage(Session session) throws JMSException { + return getRequiredMessageConverter().toMessage(message, session); + } + }); + } + + public void convertAndSend(Object message, MessagePostProcessor postProcessor) throws JmsException { + Destination defaultDestination = getDefaultDestination(); + if (defaultDestination != null) { + convertAndSend(defaultDestination, message, postProcessor); + } + else { + convertAndSend(getRequiredDefaultDestinationName(), message, postProcessor); + } + } + + public void convertAndSend( + Destination destination, final Object message, final MessagePostProcessor postProcessor) + throws JmsException { + + send(destination, new MessageCreator() { + public Message createMessage(Session session) throws JMSException { + Message msg = getRequiredMessageConverter().toMessage(message, session); + return postProcessor.postProcessMessage(msg); + } + }); + } + + public void convertAndSend( + String destinationName, final Object message, final MessagePostProcessor postProcessor) + throws JmsException { + + send(destinationName, new MessageCreator() { + public Message createMessage(Session session) throws JMSException { + Message msg = getRequiredMessageConverter().toMessage(message, session); + return postProcessor.postProcessMessage(msg); + } + }); + } + + + //------------------------------------------------------------------------- + // Convenience methods for receiving messages + //------------------------------------------------------------------------- + + public Message receive() throws JmsException { + Destination defaultDestination = getDefaultDestination(); + if (defaultDestination != null) { + return receive(defaultDestination); + } + else { + return receive(getRequiredDefaultDestinationName()); + } + } + + public Message receive(Destination destination) throws JmsException { + return receiveSelected(destination, null); + } + + public Message receive(String destinationName) throws JmsException { + return receiveSelected(destinationName, null); + } + + public Message receiveSelected(String messageSelector) throws JmsException { + Destination defaultDestination = getDefaultDestination(); + if (defaultDestination != null) { + return receiveSelected(defaultDestination, messageSelector); + } + else { + return receiveSelected(getRequiredDefaultDestinationName(), messageSelector); + } + } + + public Message receiveSelected(final Destination destination, final String messageSelector) throws JmsException { + return (Message) execute(new SessionCallback() { + public Object doInJms(Session session) throws JMSException { + return doReceive(session, destination, messageSelector); + } + }, true); + } + + public Message receiveSelected(final String destinationName, final String messageSelector) throws JmsException { + return (Message) execute(new SessionCallback() { + public Object doInJms(Session session) throws JMSException { + Destination destination = resolveDestinationName(session, destinationName); + return doReceive(session, destination, messageSelector); + } + }, true); + } + + /** + * Receive a JMS message. + * @param session the JMS Session to operate on + * @param destination the JMS Destination to receive from + * @param messageSelector the message selector for this consumer (can be null) + * @return the JMS Message received, or null if none + * @throws JMSException if thrown by JMS API methods + */ + protected Message doReceive(Session session, Destination destination, String messageSelector) + throws JMSException { + + return doReceive(session, createConsumer(session, destination, messageSelector)); + } + + /** + * Actually receive a JMS message. + * @param session the JMS Session to operate on + * @param consumer the JMS MessageConsumer to send with + * @return the JMS Message received, or null if none + * @throws JMSException if thrown by JMS API methods + */ + protected Message doReceive(Session session, MessageConsumer consumer) throws JMSException { + try { + // Use transaction timeout (if available). + long timeout = getReceiveTimeout(); + JmsResourceHolder resourceHolder = + (JmsResourceHolder) TransactionSynchronizationManager.getResource(getConnectionFactory()); + if (resourceHolder != null && resourceHolder.hasTimeout()) { + timeout = resourceHolder.getTimeToLiveInMillis(); + } + + Message message = null; + if (timeout == RECEIVE_TIMEOUT_NO_WAIT) { + message = consumer.receiveNoWait(); + } + else if (timeout > 0) { + message = consumer.receive(timeout); + } + else { + message = consumer.receive(); + } + + if (session.getTransacted()) { + // Commit necessary - but avoid commit call within a JTA transaction. + if (isSessionLocallyTransacted(session)) { + // Transacted session created by this template -> commit. + JmsUtils.commitIfNecessary(session); + } + } + else if (isClientAcknowledge(session)) { + // Manually acknowledge message, if any. + if (message != null) { + message.acknowledge(); + } + } + return message; + } + finally { + JmsUtils.closeMessageConsumer(consumer); + } + } + + + //------------------------------------------------------------------------- + // Convenience methods for receiving auto-converted messages + //------------------------------------------------------------------------- + + public Object receiveAndConvert() throws JmsException { + return doConvertFromMessage(receive()); + } + + public Object receiveAndConvert(Destination destination) throws JmsException { + return doConvertFromMessage(receive(destination)); + } + + public Object receiveAndConvert(String destinationName) throws JmsException { + return doConvertFromMessage(receive(destinationName)); + } + + public Object receiveSelectedAndConvert(String messageSelector) throws JmsException { + return doConvertFromMessage(receiveSelected(messageSelector)); + } + + public Object receiveSelectedAndConvert(Destination destination, String messageSelector) throws JmsException { + return doConvertFromMessage(receiveSelected(destination, messageSelector)); + } + + public Object receiveSelectedAndConvert(String destinationName, String messageSelector) throws JmsException { + return doConvertFromMessage(receiveSelected(destinationName, messageSelector)); + } + + /** + * Extract the content from the given JMS message. + * @param message the JMS Message to convert (can be null) + * @return the content of the message, or null if none + */ + protected Object doConvertFromMessage(Message message) { + if (message != null) { + try { + return getRequiredMessageConverter().fromMessage(message); + } + catch (JMSException ex) { + throw convertJmsAccessException(ex); + } + } + return null; + } + + + //------------------------------------------------------------------------- + // Convenience methods for browsing messages + //------------------------------------------------------------------------- + + public Object browse(BrowserCallback action) throws JmsException { + Queue defaultQueue = getDefaultQueue(); + if (defaultQueue != null) { + return browse(defaultQueue, action); + } + else { + return browse(getRequiredDefaultDestinationName(), action); + } + } + + public Object browse(Queue queue, BrowserCallback action) throws JmsException { + return browseSelected(queue, null, action); + } + + public Object browse(String queueName, BrowserCallback action) throws JmsException { + return browseSelected(queueName, null, action); + } + + public Object browseSelected(String messageSelector, BrowserCallback action) throws JmsException { + Queue defaultQueue = getDefaultQueue(); + if (defaultQueue != null) { + return browseSelected(defaultQueue, messageSelector, action); + } + else { + return browseSelected(getRequiredDefaultDestinationName(), messageSelector, action); + } + } + + public Object browseSelected(final Queue queue, final String messageSelector, final BrowserCallback action) + throws JmsException { + + Assert.notNull(action, "Callback object must not be null"); + return execute(new SessionCallback() { + public Object doInJms(Session session) throws JMSException { + QueueBrowser browser = createBrowser(session, queue, messageSelector); + try { + return action.doInJms(session, browser); + } + finally { + JmsUtils.closeQueueBrowser(browser); + } + } + }, true); + } + + public Object browseSelected(final String queueName, final String messageSelector, final BrowserCallback action) + throws JmsException { + + Assert.notNull(action, "Callback object must not be null"); + return execute(new SessionCallback() { + public Object doInJms(Session session) throws JMSException { + Queue queue = (Queue) getDestinationResolver().resolveDestinationName(session, queueName, false); + QueueBrowser browser = createBrowser(session, queue, messageSelector); + try { + return action.doInJms(session, browser); + } + finally { + JmsUtils.closeQueueBrowser(browser); + } + } + }, true); + } + + + //------------------------------------------------------------------------- + // JMS 1.1 factory methods, potentially overridden for JMS 1.0.2 + //------------------------------------------------------------------------- + + /** + * Fetch an appropriate Connection from the given JmsResourceHolder. + *

This implementation accepts any JMS 1.1 Connection. + * @param holder the JmsResourceHolder + * @return an appropriate Connection fetched from the holder, + * or null if none found + */ + protected Connection getConnection(JmsResourceHolder holder) { + return holder.getConnection(); + } + + /** + * Fetch an appropriate Session from the given JmsResourceHolder. + *

This implementation accepts any JMS 1.1 Session. + * @param holder the JmsResourceHolder + * @return an appropriate Session fetched from the holder, + * or null if none found + */ + protected Session getSession(JmsResourceHolder holder) { + return holder.getSession(); + } + + /** + * Check whether the given Session is locally transacted, that is, whether + * its transaction is managed by this listener container's Session handling + * and not by an external transaction coordinator. + *

Note: The Session's own transacted flag will already have been checked + * before. This method is about finding out whether the Session's transaction + * is local or externally coordinated. + * @param session the Session to check + * @return whether the given Session is locally transacted + * @see #isSessionTransacted() + * @see org.springframework.jms.connection.ConnectionFactoryUtils#isSessionTransactional + */ + protected boolean isSessionLocallyTransacted(Session session) { + return isSessionTransacted() && + !ConnectionFactoryUtils.isSessionTransactional(session, getConnectionFactory()); + } + + /** + * Create a JMS MessageProducer for the given Session and Destination, + * configuring it to disable message ids and/or timestamps (if necessary). + *

Delegates to {@link #doCreateProducer} for creation of the raw + * JMS MessageProducer, which needs to be specific to JMS 1.1 or 1.0.2. + * @param session the JMS Session to create a MessageProducer for + * @param destination the JMS Destination to create a MessageProducer for + * @return the new JMS MessageProducer + * @throws JMSException if thrown by JMS API methods + * @see #setMessageIdEnabled + * @see #setMessageTimestampEnabled + */ + protected MessageProducer createProducer(Session session, Destination destination) throws JMSException { + MessageProducer producer = doCreateProducer(session, destination); + if (!isMessageIdEnabled()) { + producer.setDisableMessageID(true); + } + if (!isMessageTimestampEnabled()) { + producer.setDisableMessageTimestamp(true); + } + return producer; + } + + /** + * Create a raw JMS MessageProducer for the given Session and Destination. + *

This implementation uses JMS 1.1 API. + * @param session the JMS Session to create a MessageProducer for + * @param destination the JMS Destination to create a MessageProducer for + * @return the new JMS MessageProducer + * @throws JMSException if thrown by JMS API methods + */ + protected MessageProducer doCreateProducer(Session session, Destination destination) throws JMSException { + return session.createProducer(destination); + } + + /** + * Create a JMS MessageConsumer for the given Session and Destination. + *

This implementation uses JMS 1.1 API. + * @param session the JMS Session to create a MessageConsumer for + * @param destination the JMS Destination to create a MessageConsumer for + * @param messageSelector the message selector for this consumer (can be null) + * @return the new JMS MessageConsumer + * @throws JMSException if thrown by JMS API methods + */ + protected MessageConsumer createConsumer(Session session, Destination destination, String messageSelector) + throws JMSException { + + // Only pass in the NoLocal flag in case of a Topic: + // Some JMS providers, such as WebSphere MQ 6.0, throw IllegalStateException + // in case of the NoLocal flag being specified for a Queue. + if (isPubSubDomain()) { + return session.createConsumer(destination, messageSelector, isPubSubNoLocal()); + } + else { + return session.createConsumer(destination, messageSelector); + } + } + + /** + * Create a JMS MessageProducer for the given Session and Destination, + * configuring it to disable message ids and/or timestamps (if necessary). + *

Delegates to {@link #doCreateProducer} for creation of the raw + * JMS MessageProducer, which needs to be specific to JMS 1.1 or 1.0.2. + * @param session the JMS Session to create a QueueBrowser for + * @param queue the JMS Queue to create a QueueBrowser for + * @param messageSelector the message selector for this consumer (can be null) + * @return the new JMS QueueBrowser + * @throws JMSException if thrown by JMS API methods + * @see #setMessageIdEnabled + * @see #setMessageTimestampEnabled + */ + protected QueueBrowser createBrowser(Session session, Queue queue, String messageSelector) + throws JMSException { + + return session.createBrowser(queue, messageSelector); + } + + + /** + * ResourceFactory implementation that delegates to this template's protected callback methods. + */ + private class JmsTemplateResourceFactory implements ConnectionFactoryUtils.ResourceFactory { + + public Connection getConnection(JmsResourceHolder holder) { + return JmsTemplate.this.getConnection(holder); + } + + public Session getSession(JmsResourceHolder holder) { + return JmsTemplate.this.getSession(holder); + } + + public Connection createConnection() throws JMSException { + return JmsTemplate.this.createConnection(); + } + + public Session createSession(Connection con) throws JMSException { + return JmsTemplate.this.createSession(con); + } + + public boolean isSynchedLocalTransactionAllowed() { + return JmsTemplate.this.isSessionTransacted(); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/JmsTemplate102.java b/org.springframework.jms/src/main/java/org/springframework/jms/core/JmsTemplate102.java new file mode 100644 index 00000000000..91026029614 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/JmsTemplate102.java @@ -0,0 +1,255 @@ +/* + * Copyright 2002-2008 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.core; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.QueueBrowser; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.QueueSender; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; +import javax.jms.TopicPublisher; +import javax.jms.TopicSession; + +import org.springframework.jms.connection.JmsResourceHolder; +import org.springframework.jms.support.converter.SimpleMessageConverter102; + +/** + * A subclass of {@link JmsTemplate} for the JMS 1.0.2 specification, not relying + * on JMS 1.1 methods like JmsTemplate itself. This class can be used for JMS + * 1.0.2 providers, offering the same API as JmsTemplate does for JMS 1.1 providers. + * + *

You must specify the domain (or style) of messaging to be either + * Point-to-Point (Queues) or Publish/Subscribe (Topics), using the + * {@link #setPubSubDomain "pubSubDomain" property}. + * Point-to-Point (Queues) is the default domain. + * + *

The "pubSubDomain" property is an important setting due to the use of similar + * but separate class hierarchies in the JMS 1.0.2 API. JMS 1.1 provides a new + * domain-independent API that allows for easy mix-and-match use of Point-to-Point + * and Publish/Subscribe styles. + * + *

This template uses a + * {@link org.springframework.jms.support.destination.DynamicDestinationResolver} + * and a {@link org.springframework.jms.support.converter.SimpleMessageConverter102} + * as default strategies for resolving a destination name and converting a message, + * respectively. + * + * @author Mark Pollack + * @author Juergen Hoeller + * @since 1.1 + * @see #setConnectionFactory + * @see #setPubSubDomain + * @see javax.jms.Queue + * @see javax.jms.Topic + * @see javax.jms.QueueSession + * @see javax.jms.TopicSession + * @see javax.jms.QueueSender + * @see javax.jms.TopicPublisher + * @see javax.jms.QueueReceiver + * @see javax.jms.TopicSubscriber + */ +public class JmsTemplate102 extends JmsTemplate { + + /** + * Create a new JmsTemplate102 for bean-style usage. + *

Note: The ConnectionFactory has to be set before using the instance. + * This constructor can be used to prepare a JmsTemplate via a BeanFactory, + * typically setting the ConnectionFactory via setConnectionFactory. + * @see #setConnectionFactory + */ + public JmsTemplate102() { + super(); + } + + /** + * Create a new JmsTemplate102, given a ConnectionFactory. + * @param connectionFactory the ConnectionFactory to obtain Connections from + * @param pubSubDomain whether the Publish/Subscribe domain (Topics) or + * Point-to-Point domain (Queues) should be used + * @see #setPubSubDomain + */ + public JmsTemplate102(ConnectionFactory connectionFactory, boolean pubSubDomain) { + this(); + setConnectionFactory(connectionFactory); + setPubSubDomain(pubSubDomain); + afterPropertiesSet(); + } + + /** + * Initialize the default implementations for the template's strategies: + * DynamicDestinationResolver and SimpleMessageConverter102. + * @see #setDestinationResolver + * @see #setMessageConverter + * @see org.springframework.jms.support.destination.DynamicDestinationResolver + * @see org.springframework.jms.support.converter.SimpleMessageConverter102 + */ + protected void initDefaultStrategies() { + setMessageConverter(new SimpleMessageConverter102()); + } + + /** + * In addition to checking if the connection factory is set, make sure + * that the supplied connection factory is of the appropriate type for + * the specified destination type: QueueConnectionFactory for queues, + * and TopicConnectionFactory for topics. + */ + public void afterPropertiesSet() { + super.afterPropertiesSet(); + + // Make sure that the ConnectionFactory passed is consistent. + // Some provider implementations of the ConnectionFactory interface + // implement both domain interfaces under the cover, so just check if + // the selected domain is consistent with the type of connection factory. + if (isPubSubDomain()) { + if (!(getConnectionFactory() instanceof TopicConnectionFactory)) { + throw new IllegalArgumentException( + "Specified a Spring JMS 1.0.2 template for topics " + + "but did not supply an instance of TopicConnectionFactory"); + } + } + else { + if (!(getConnectionFactory() instanceof QueueConnectionFactory)) { + throw new IllegalArgumentException( + "Specified a Spring JMS 1.0.2 template for queues " + + "but did not supply an instance of QueueConnectionFactory"); + } + } + } + + + /** + * This implementation overrides the superclass method to accept either + * a QueueConnection or a TopicConnection, depending on the domain. + */ + protected Connection getConnection(JmsResourceHolder holder) { + return holder.getConnection(isPubSubDomain() ? (Class) TopicConnection.class : QueueConnection.class); + } + + /** + * This implementation overrides the superclass method to accept either + * a QueueSession or a TopicSession, depending on the domain. + */ + protected Session getSession(JmsResourceHolder holder) { + return holder.getSession(isPubSubDomain() ? (Class) TopicSession.class : QueueSession.class); + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Connection createConnection() throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnectionFactory) getConnectionFactory()).createTopicConnection(); + } + else { + return ((QueueConnectionFactory) getConnectionFactory()).createQueueConnection(); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Session createSession(Connection con) throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnection) con).createTopicSession(isSessionTransacted(), getSessionAcknowledgeMode()); + } + else { + return ((QueueConnection) con).createQueueSession(isSessionTransacted(), getSessionAcknowledgeMode()); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected MessageProducer doCreateProducer(Session session, Destination destination) throws JMSException { + if (isPubSubDomain()) { + return ((TopicSession) session).createPublisher((Topic) destination); + } + else { + return ((QueueSession) session).createSender((Queue) destination); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected MessageConsumer createConsumer(Session session, Destination destination, String messageSelector) + throws JMSException { + + if (isPubSubDomain()) { + return ((TopicSession) session).createSubscriber((Topic) destination, messageSelector, isPubSubNoLocal()); + } + else { + return ((QueueSession) session).createReceiver((Queue) destination, messageSelector); + } + } + + protected QueueBrowser createBrowser(Session session, Queue queue, String messageSelector) + throws JMSException { + + if (isPubSubDomain()) { + throw new javax.jms.IllegalStateException("Cannot create QueueBrowser for a TopicSession"); + } + else { + return ((QueueSession) session).createBrowser(queue, messageSelector); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected void doSend(MessageProducer producer, Message message) throws JMSException { + if (isPubSubDomain()) { + if (isExplicitQosEnabled()) { + ((TopicPublisher) producer).publish(message, getDeliveryMode(), getPriority(), getTimeToLive()); + } + else { + ((TopicPublisher) producer).publish(message); + } + } + else { + if (isExplicitQosEnabled()) { + ((QueueSender) producer).send(message, getDeliveryMode(), getPriority(), getTimeToLive()); + } + else { + ((QueueSender) producer).send(message); + } + } + } + + /** + * This implementation overrides the superclass method to avoid using + * JMS 1.1's Session getAcknowledgeMode() method. + * The best we can do here is to check the setting on the template. + * @see #getSessionAcknowledgeMode() + */ + protected boolean isClientAcknowledge(Session session) throws JMSException { + return (getSessionAcknowledgeMode() == Session.CLIENT_ACKNOWLEDGE); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/MessageCreator.java b/org.springframework.jms/src/main/java/org/springframework/jms/core/MessageCreator.java new file mode 100644 index 00000000000..fec95b353bf --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/MessageCreator.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2005 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.core; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Session; + +/** + * Creates a JMS message given a {@link Session}. + * + *

The Session typically is provided by an instance + * of the {@link JmsTemplate} class. + * + *

Implementations do not need to concern themselves with + * checked JMSExceptions (from the 'javax.jms' + * package) that may be thrown from operations they attempt. The + * JmsTemplate will catch and handle these + * JMSExceptions appropriately. + * + * @author Mark Pollack + * @since 1.1 + */ +public interface MessageCreator { + + /** + * Create a {@link Message} to be sent. + * @param session the JMS {@link Session} to be used to create the + * Message (never null) + * @return the Message to be sent + * @throws javax.jms.JMSException if thrown by JMS API methods + */ + Message createMessage(Session session) throws JMSException; + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/MessagePostProcessor.java b/org.springframework.jms/src/main/java/org/springframework/jms/core/MessagePostProcessor.java new file mode 100644 index 00000000000..ed8a8cc766e --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/MessagePostProcessor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2005 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.core; + +import javax.jms.JMSException; +import javax.jms.Message; + +/** + * To be used with JmsTemplate's send method that convert an object to a message. + * It allows for further modification of the message after it has been processed + * by the converter. This is useful for setting of JMS Header and Properties. + * + *

This often as an anonymous class within a method implementation. + * + * @author Mark Pollack + * @since 1.1 + * @see JmsTemplate#convertAndSend(String, Object, MessagePostProcessor) + * @see JmsTemplate#convertAndSend(javax.jms.Destination, Object, MessagePostProcessor) + * @see org.springframework.jms.support.converter.MessageConverter + */ +public interface MessagePostProcessor { + + /** + * Apply a MessagePostProcessor to the message. The returned message is + * typically a modified version of the original. + * @param message the JMS message from the MessageConverter + * @return the modified version of the Message + * @throws javax.jms.JMSException if thrown by JMS API methods + */ + Message postProcessMessage(Message message) throws JMSException; + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/ProducerCallback.java b/org.springframework.jms/src/main/java/org/springframework/jms/core/ProducerCallback.java new file mode 100644 index 00000000000..0c9cd483d6b --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/ProducerCallback.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2008 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.core; + +import javax.jms.JMSException; +import javax.jms.MessageProducer; +import javax.jms.Session; + +/** + * Callback for sending a message to a JMS destination. + * + *

To be used with JmsTemplate's callback methods that take a ProducerCallback + * argument, often implemented as an anonymous inner class. + * + *

The typical implementation will perform multiple operations on the + * supplied JMS {@link Session} and {@link MessageProducer}. When used with + * a 1.0.2 provider, you need to downcast to the appropriate domain + * implementation, either {@link javax.jms.QueueSender} or + * {@link javax.jms.TopicPublisher}, to actually send a message. + * + * @author Mark Pollack + * @since 1.1 + * @see JmsTemplate#execute(ProducerCallback) + * @see JmsTemplate#execute(javax.jms.Destination, ProducerCallback) + * @see JmsTemplate#execute(String, ProducerCallback) + */ +public interface ProducerCallback { + + /** + * Perform operations on the given {@link Session} and {@link MessageProducer}. + *

The message producer is not associated with any destination unless + * when specified in the JmsTemplate call. + * @param session the JMS Session object to use + * @param producer the JMS MessageProducer object to use + * @return a result object from working with the Session, if any (can be null) + * @throws javax.jms.JMSException if thrown by JMS API methods + */ + Object doInJms(Session session, MessageProducer producer) throws JMSException; + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/SessionCallback.java b/org.springframework.jms/src/main/java/org/springframework/jms/core/SessionCallback.java new file mode 100644 index 00000000000..296fad63008 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/SessionCallback.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2005 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.core; + +import javax.jms.JMSException; +import javax.jms.Session; + +/** + * Callback for executing any number of operations on a provided + * {@link Session}. + * + *

To be used with the {@link JmsTemplate#execute(SessionCallback)} + * method, often implemented as an anonymous inner class. + * + * @author Mark Pollack + * @since 1.1 + * @see JmsTemplate#execute(SessionCallback) + */ +public interface SessionCallback { + + /** + * Execute any number of operations against the supplied JMS + * {@link Session}, possibly returning a result. + * @param session the JMS Session + * @return a result object from working with the Session, if any (so can be null) + * @throws javax.jms.JMSException if thrown by JMS API methods + */ + Object doInJms(Session session) throws JMSException; + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/core/package.html new file mode 100644 index 00000000000..4a32b2f946c --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/package.html @@ -0,0 +1,8 @@ + + + +Core package of the JMS support. +Provides a JmsTemplate class and various callback interfaces. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java b/org.springframework.jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java new file mode 100644 index 00000000000..a8dcc8d547a --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2005 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.core.support; + +import javax.jms.ConnectionFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jms.core.JmsTemplate; + +/** + * Convenient super class for application classes that need JMS access. + * + *

Requires a ConnectionFactory or a JmsTemplate instance to be set. + * It will create its own JmsTemplate if a ConnectionFactory is passed in. + * A custom JmsTemplate instance can be created for a given ConnectionFactory + * through overriding the createJmsTemplate method. + * + * @author Mark Pollack + * @since 1.1.1 + * @see #setConnectionFactory + * @see #setJmsTemplate + * @see #createJmsTemplate + * @see org.springframework.jms.core.JmsTemplate + */ +public abstract class JmsGatewaySupport implements InitializingBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private JmsTemplate jmsTemplate; + + + /** + * Set the JMS connection factory to be used by the gateway. + * Will automatically create a JmsTemplate for the given ConnectionFactory. + * @see #createJmsTemplate + * @see #setConnectionFactory(javax.jms.ConnectionFactory) + * @param connectionFactory + */ + public final void setConnectionFactory(ConnectionFactory connectionFactory) { + this.jmsTemplate = createJmsTemplate(connectionFactory); + } + + /** + * Create a JmsTemplate for the given ConnectionFactory. + * Only invoked if populating the gateway with a ConnectionFactory reference. + *

Can be overridden in subclasses to provide a JmsTemplate instance with + * a different configuration or the JMS 1.0.2 version, JmsTemplate102. + * @param connectionFactory the JMS ConnectionFactory to create a JmsTemplate for + * @return the new JmsTemplate instance + * @see #setConnectionFactory + * @see org.springframework.jms.core.JmsTemplate102 + */ + protected JmsTemplate createJmsTemplate(ConnectionFactory connectionFactory) { + return new JmsTemplate(connectionFactory); + } + + /** + * Return the JMS ConnectionFactory used by the gateway. + */ + public final ConnectionFactory getConnectionFactory() { + return (this.jmsTemplate != null ? this.jmsTemplate.getConnectionFactory() : null); + } + + /** + * Set the JmsTemplate for the gateway. + * @param jmsTemplate + * @see #setConnectionFactory(javax.jms.ConnectionFactory) + */ + public final void setJmsTemplate(JmsTemplate jmsTemplate) { + this.jmsTemplate = jmsTemplate; + } + + /** + * Return the JmsTemplate for the gateway. + */ + public final JmsTemplate getJmsTemplate() { + return jmsTemplate; + } + + public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException { + if (this.jmsTemplate == null) { + throw new IllegalArgumentException("connectionFactory or jmsTemplate is required"); + } + try { + initGateway(); + } + catch (Exception ex) { + throw new BeanInitializationException("Initialization of JMS gateway failed: " + ex.getMessage(), ex); + } + } + + /** + * Subclasses can override this for custom initialization behavior. + * Gets called after population of this instance's bean properties. + * @throws java.lang.Exception if initialization fails + */ + protected void initGateway() throws Exception { + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/core/support/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/core/support/package.html new file mode 100644 index 00000000000..7863ad48e00 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/core/support/package.html @@ -0,0 +1,8 @@ + + + +Classes supporting the org.springframework.jms.core package. +Contains a base class for JmsTemplate usage. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java new file mode 100644 index 00000000000..b50d889f411 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java @@ -0,0 +1,609 @@ +/* + * Copyright 2002-2008 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 java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.jms.Connection; +import javax.jms.JMSException; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.context.Lifecycle; +import org.springframework.jms.JmsException; +import org.springframework.jms.connection.ConnectionFactoryUtils; +import org.springframework.jms.support.JmsUtils; +import org.springframework.jms.support.destination.JmsDestinationAccessor; +import org.springframework.util.ClassUtils; + +/** + * Common base class for all containers which need to implement listening + * based on a JMS Connection (either shared or freshly obtained for each attempt). + * Inherits basic Connection and Session configuration handling from the + * {@link org.springframework.jms.support.JmsAccessor} base class. + * + *

This class provides basic lifecycle management, in particular management + * of a shared JMS Connection. Subclasses are supposed to plug into this + * lifecycle, implementing the {@link #sharedConnectionEnabled()} as well + * as the {@link #doInitialize()} and {@link #doShutdown()} template methods. + * + *

This base class does not assume any specific listener programming model + * or listener invoker mechanism. It just provides the general runtime + * lifecycle management needed for any kind of JMS-based listening mechanism + * that operates on a JMS Connection/Session. + * + *

For a concrete listener programming model, check out the + * {@link AbstractMessageListenerContainer} subclass. For a concrete listener + * invoker mechanism, check out the {@link DefaultMessageListenerContainer} class. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see #sharedConnectionEnabled() + * @see #doInitialize() + * @see #doShutdown() + */ +public abstract class AbstractJmsListeningContainer extends JmsDestinationAccessor + implements Lifecycle, BeanNameAware, DisposableBean { + + private String clientId; + + private boolean autoStartup = true; + + private String beanName; + + private Connection sharedConnection; + + private boolean sharedConnectionStarted = false; + + protected final Object sharedConnectionMonitor = new Object(); + + private boolean active = false; + + private boolean running = false; + + private final List pausedTasks = new LinkedList(); + + protected final Object lifecycleMonitor = new Object(); + + + /** + * Specify the JMS client id for a shared Connection created and used + * by this container. + *

Note that client ids need to be unique among all active Connections + * of the underlying JMS provider. Furthermore, a client id can only be + * assigned if the original ConnectionFactory hasn't already assigned one. + * @see javax.jms.Connection#setClientID + * @see #setConnectionFactory + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * Return the JMS client ID for the shared Connection created and used + * by this container, if any. + */ + public String getClientId() { + return this.clientId; + } + + /** + * Set whether to automatically start the container after initialization. + *

Default is "true"; set this to "false" to allow for manual startup + * through the {@link #start()} method. + */ + public void setAutoStartup(boolean autoStartup) { + this.autoStartup = autoStartup; + } + + public void setBeanName(String beanName) { + this.beanName = beanName; + } + + /** + * Return the bean name that this listener container has been assigned + * in its containing bean factory, if any. + */ + protected final String getBeanName() { + return this.beanName; + } + + + /** + * Delegates to {@link #validateConfiguration()} and {@link #initialize()}. + */ + public void afterPropertiesSet() { + super.afterPropertiesSet(); + validateConfiguration(); + initialize(); + } + + /** + * Validate the configuration of this container. + *

The default implementation is empty. To be overridden in subclasses. + */ + protected void validateConfiguration() { + } + + /** + * Calls {@link #shutdown()} when the BeanFactory destroys the container instance. + * @see #shutdown() + */ + public void destroy() { + shutdown(); + } + + + //------------------------------------------------------------------------- + // Lifecycle methods for starting and stopping the container + //------------------------------------------------------------------------- + + /** + * Initialize this container. + *

Creates a JMS Connection, starts the {@link javax.jms.Connection} + * (if {@link #setAutoStartup(boolean) "autoStartup"} hasn't been turned off), + * and calls {@link #doInitialize()}. + * @throws org.springframework.jms.JmsException if startup failed + */ + public void initialize() throws JmsException { + try { + synchronized (this.lifecycleMonitor) { + this.active = true; + this.lifecycleMonitor.notifyAll(); + } + if (this.autoStartup) { + doStart(); + } + doInitialize(); + } + catch (JMSException ex) { + synchronized (this.sharedConnectionMonitor) { + ConnectionFactoryUtils.releaseConnection(this.sharedConnection, getConnectionFactory(), this.autoStartup); + this.sharedConnection = null; + } + throw convertJmsAccessException(ex); + } + } + + /** + * Stop the shared Connection, call {@link #doShutdown()}, + * and close this container. + * @throws JmsException if shutdown failed + */ + public void shutdown() throws JmsException { + logger.debug("Shutting down JMS listener container"); + boolean wasRunning = false; + synchronized (this.lifecycleMonitor) { + wasRunning = this.running; + this.running = false; + this.active = false; + this.lifecycleMonitor.notifyAll(); + } + + // Stop shared Connection early, if necessary. + if (wasRunning && sharedConnectionEnabled()) { + try { + stopSharedConnection(); + } + catch (Throwable ex) { + logger.debug("Could not stop JMS Connection on shutdown", ex); + } + } + + // Shut down the invokers. + try { + doShutdown(); + } + catch (JMSException ex) { + throw convertJmsAccessException(ex); + } + finally { + if (sharedConnectionEnabled()) { + synchronized (this.sharedConnectionMonitor) { + ConnectionFactoryUtils.releaseConnection(this.sharedConnection, getConnectionFactory(), false); + this.sharedConnection = null; + } + } + } + } + + /** + * Return whether this container is currently active, + * that is, whether it has been set up but not shut down yet. + */ + public final boolean isActive() { + synchronized (this.lifecycleMonitor) { + return this.active; + } + } + + /** + * Start this container. + * @throws JmsException if starting failed + * @see #doStart + */ + public void start() throws JmsException { + try { + doStart(); + } + catch (JMSException ex) { + throw convertJmsAccessException(ex); + } + } + + /** + * Start the shared Connection, if any, and notify all invoker tasks. + * @throws JMSException if thrown by JMS API methods + * @see #startSharedConnection + */ + protected void doStart() throws JMSException { + // Lazily establish a shared Connection, if necessary. + if (sharedConnectionEnabled()) { + establishSharedConnection(); + } + + // Reschedule paused tasks, if any. + synchronized (this.lifecycleMonitor) { + this.running = true; + this.lifecycleMonitor.notifyAll(); + resumePausedTasks(); + } + + // Start the shared Connection, if any. + if (sharedConnectionEnabled()) { + startSharedConnection(); + } + } + + /** + * Stop this container. + * @throws JmsException if stopping failed + * @see #doStop + */ + public void stop() throws JmsException { + try { + doStop(); + } + catch (JMSException ex) { + throw convertJmsAccessException(ex); + } + } + + /** + * Notify all invoker tasks and stop the shared Connection, if any. + * @throws JMSException if thrown by JMS API methods + * @see #stopSharedConnection + */ + protected void doStop() throws JMSException { + synchronized (this.lifecycleMonitor) { + this.running = false; + this.lifecycleMonitor.notifyAll(); + } + + if (sharedConnectionEnabled()) { + stopSharedConnection(); + } + } + + /** + * Determine whether this container is currently running, + * that is, whether it has been started and not stopped yet. + * @see #start() + * @see #stop() + * @see #runningAllowed() + */ + public final boolean isRunning() { + synchronized (this.lifecycleMonitor) { + return (this.running && runningAllowed()); + } + } + + /** + * Check whether this container's listeners are generally allowed to run. + *

This implementation always returns true; the default 'running' + * state is purely determined by {@link #start()} / {@link #stop()}. + *

Subclasses may override this method to check against temporary + * conditions that prevent listeners from actually running. In other words, + * they may apply further restrictions to the 'running' state, returning + * false if such a restriction prevents listeners from running. + */ + protected boolean runningAllowed() { + return true; + } + + + //------------------------------------------------------------------------- + // Management of a shared JMS Connection + //------------------------------------------------------------------------- + + /** + * Establish a shared Connection for this container. + *

The default implementation delegates to {@link #createSharedConnection()}, + * which does one immediate attempt and throws an exception if it fails. + * Can be overridden to have a recovery process in place, retrying + * until a Connection can be successfully established. + * @throws JMSException if thrown by JMS API methods + */ + protected void establishSharedConnection() throws JMSException { + synchronized (this.sharedConnectionMonitor) { + if (this.sharedConnection == null) { + this.sharedConnection = createSharedConnection(); + logger.debug("Established shared JMS Connection"); + } + } + } + + /** + * Refresh the shared Connection that this container holds. + *

Called on startup and also after an infrastructure exception + * that occurred during invoker setup and/or execution. + * @throws JMSException if thrown by JMS API methods + */ + protected final void refreshSharedConnection() throws JMSException { + synchronized (this.sharedConnectionMonitor) { + ConnectionFactoryUtils.releaseConnection( + this.sharedConnection, getConnectionFactory(), this.sharedConnectionStarted); + this.sharedConnection = null; + this.sharedConnection = createSharedConnection(); + if (this.sharedConnectionStarted) { + this.sharedConnection.start(); + } + } + } + + /** + * Create a shared Connection for this container. + *

The default implementation creates a standard Connection + * and prepares it through {@link #prepareSharedConnection}. + * @return the prepared Connection + * @throws JMSException if the creation failed + */ + protected Connection createSharedConnection() throws JMSException { + Connection con = createConnection(); + try { + prepareSharedConnection(con); + return con; + } + catch (JMSException ex) { + JmsUtils.closeConnection(con); + throw ex; + } + } + + /** + * Prepare the given Connection, which is about to be registered + * as shared Connection for this container. + *

The default implementation sets the specified client id, if any. + * Subclasses can override this to apply further settings. + * @param connection the Connection to prepare + * @throws JMSException if the preparation efforts failed + * @see #getClientId() + */ + protected void prepareSharedConnection(Connection connection) throws JMSException { + String clientId = getClientId(); + if (clientId != null) { + connection.setClientID(clientId); + } + } + + /** + * Start the shared Connection. + * @throws JMSException if thrown by JMS API methods + * @see javax.jms.Connection#start() + */ + protected void startSharedConnection() throws JMSException { + synchronized (this.sharedConnectionMonitor) { + this.sharedConnectionStarted = true; + if (this.sharedConnection != null) { + try { + this.sharedConnection.start(); + } + catch (javax.jms.IllegalStateException ex) { + logger.debug("Ignoring Connection start exception - assuming already started: " + ex); + } + } + } + } + + /** + * Stop the shared Connection. + * @throws JMSException if thrown by JMS API methods + * @see javax.jms.Connection#start() + */ + protected void stopSharedConnection() throws JMSException { + synchronized (this.sharedConnectionMonitor) { + this.sharedConnectionStarted = false; + if (this.sharedConnection != null) { + try { + this.sharedConnection.stop(); + } + catch (javax.jms.IllegalStateException ex) { + logger.debug("Ignoring Connection stop exception - assuming already stopped: " + ex); + } + } + } + } + + /** + * Return the shared JMS Connection maintained by this container. + * Available after initialization. + * @return the shared Connection (never null) + * @throws IllegalStateException if this container does not maintain a + * shared Connection, or if the Connection hasn't been initialized yet + * @see #sharedConnectionEnabled() + */ + protected final Connection getSharedConnection() { + if (!sharedConnectionEnabled()) { + throw new IllegalStateException( + "This listener container does not maintain a shared Connection"); + } + synchronized (this.sharedConnectionMonitor) { + if (this.sharedConnection == null) { + throw new SharedConnectionNotInitializedException( + "This listener container's shared Connection has not been initialized yet"); + } + return this.sharedConnection; + } + } + + + //------------------------------------------------------------------------- + // Management of paused tasks + //------------------------------------------------------------------------- + + /** + * Take the given task object and reschedule it, either immediately if + * this container is currently running, or later once this container + * has been restarted. + *

If this container has already been shut down, the task will not + * get rescheduled at all. + * @param task the task object to reschedule + * @return whether the task has been rescheduled + * (either immediately or for a restart of this container) + * @see #doRescheduleTask + */ + protected final boolean rescheduleTaskIfNecessary(Object task) { + if (this.running) { + try { + doRescheduleTask(task); + } + catch (RuntimeException ex) { + logRejectedTask(task, ex); + this.pausedTasks.add(task); + } + return true; + } + else if (this.active) { + this.pausedTasks.add(task); + return true; + } + else { + return false; + } + } + + /** + * Try to resume all paused tasks. + * Tasks for which rescheduling failed simply remain in paused mode. + */ + protected void resumePausedTasks() { + synchronized (this.lifecycleMonitor) { + if (!this.pausedTasks.isEmpty()) { + for (Iterator it = this.pausedTasks.iterator(); it.hasNext();) { + Object task = it.next(); + try { + doRescheduleTask(task); + it.remove(); + if (logger.isDebugEnabled()) { + logger.debug("Resumed paused task: " + task); + } + } + catch (RuntimeException ex) { + logRejectedTask(task, ex); + // Keep the task in paused mode... + } + } + } + } + } + + public int getPausedTaskCount() { + synchronized (this.lifecycleMonitor) { + return this.pausedTasks.size(); + } + } + + /** + * Reschedule the given task object immediately. + *

To be implemented by subclasses if they ever call + * rescheduleTaskIfNecessary. + * This implementation throws an UnsupportedOperationException. + * @param task the task object to reschedule + * @see #rescheduleTaskIfNecessary + */ + protected void doRescheduleTask(Object task) { + throw new UnsupportedOperationException( + ClassUtils.getShortName(getClass()) + " does not support rescheduling of tasks"); + } + + /** + * Log a task that has been rejected by {@link #doRescheduleTask}. + *

The default implementation simply logs a corresponding message + * at debug level. + * @param task the rejected task object + * @param ex the exception thrown from {@link #doRescheduleTask} + */ + protected void logRejectedTask(Object task, RuntimeException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Listener container task [" + task + "] has been rejected and paused: " + ex); + } + } + + + //------------------------------------------------------------------------- + // Template methods to be implemented by subclasses + //------------------------------------------------------------------------- + + /** + * Return whether a shared JMS Connection should be maintained + * by this container base class. + * @see #getSharedConnection() + */ + protected abstract boolean sharedConnectionEnabled(); + + /** + * Register any invokers within this container. + *

Subclasses need to implement this method for their specific + * invoker management process. + *

A shared JMS Connection, if any, will already have been + * started at this point. + * @throws JMSException if registration failed + * @see #getSharedConnection() + */ + protected abstract void doInitialize() throws JMSException; + + /** + * Close the registered invokers. + *

Subclasses need to implement this method for their specific + * invoker management process. + *

A shared JMS Connection, if any, will automatically be closed + * afterwards. + * @throws JMSException if shutdown failed + * @see #shutdown() + */ + protected abstract void doShutdown() throws JMSException; + + + /** + * Exception that indicates that the initial setup of this container's + * shared JMS Connection failed. This is indicating to invokers that they need + * to establish the shared Connection themselves on first access. + */ + public static class SharedConnectionNotInitializedException extends RuntimeException { + + /** + * Create a new SharedConnectionNotInitializedException. + * @param msg the detail message + */ + protected SharedConnectionNotInitializedException(String msg) { + super(msg); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java new file mode 100644 index 00000000000..6abdb07aa9f --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java @@ -0,0 +1,676 @@ +/* + * Copyright 2002-2008 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 javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.ExceptionListener; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.Topic; + +import org.springframework.jms.support.JmsUtils; +import org.springframework.util.Assert; + +/** + * Abstract base class for message listener containers. Can either host + * a standard JMS {@link javax.jms.MessageListener} or a Spring-specific + * {@link SessionAwareMessageListener}. + * + *

Usually holds a single JMS {@link Connection} that all listeners are + * supposed to be registered on, which is the standard JMS way of managing + * listeners. Can alternatively also be used with a fresh Connection per + * listener, for J2EE-style XA-aware JMS messaging. The actual registration + * process is up to concrete subclasses. + * + *

NOTE: The default behavior of this message listener container + * is to never propagate an exception thrown by a message listener up to + * the JMS provider. Instead, it will log any such exception at the error level. + * This means that from the perspective of the attendant JMS provider no such + * listener will ever fail. + * + *

The listener container offers the following message acknowledgment options: + *

    + *
  • "sessionAcknowledgeMode" set to "AUTO_ACKNOWLEDGE" (default): + * Automatic message acknowledgment before listener execution; + * no redelivery in case of exception thrown. + *
  • "sessionAcknowledgeMode" set to "CLIENT_ACKNOWLEDGE": + * Automatic message acknowledgment after successful listener execution; + * no redelivery in case of exception thrown. + *
  • "sessionAcknowledgeMode" set to "DUPS_OK_ACKNOWLEDGE": + * Lazy message acknowledgment during or after listener execution; + * potential redelivery in case of exception thrown. + *
  • "sessionTransacted" set to "true": + * Transactional acknowledgment after successful listener execution; + * guaranteed redelivery in case of exception thrown. + *
+ * The exact behavior might vary according to the concrete listener container + * and JMS provider used. + * + *

There are two solutions to the duplicate processing problem: + *

    + *
  • Either add duplicate message detection to your listener, in the + * form of a business entity existence check or a protocol table check. This + * usually just needs to be done in case of the JMSRedelivered flag being + * set on the incoming message (else just process straightforwardly). + *
  • Or wrap the entire processing with an XA transaction, covering the + * reception of the message as well as the execution of the message listener. + * This is only supported by {@link DefaultMessageListenerContainer}, through + * specifying a "transactionManager" (typically a + * {@link org.springframework.transaction.jta.JtaTransactionManager}, with + * a corresponding XA-aware JMS {@link javax.jms.ConnectionFactory} passed in as + * "connectionFactory"). + *
+ * Note that XA transaction coordination adds significant runtime overhead, + * so it might be feasible to avoid it unless absolutely necessary. + * + *

Recommendations: + *

    + *
  • The general recommendation is to set "sessionTransacted" to "true", + * typically in combination with local database transactions triggered by the + * listener implementation, through Spring's standard transaction facilities. + * This will work nicely in Tomcat or in a standalone environment, often + * combined with custom duplicate message detection (if it is unacceptable + * to ever process the same message twice). + *
  • Alternatively, specify a + * {@link org.springframework.transaction.jta.JtaTransactionManager} as + * "transactionManager" for a fully XA-aware JMS provider - typically when + * running on a J2EE server, but also for other environments with a JTA + * transaction manager present. This will give full "exactly-once" guarantees + * without custom duplicate message checks, at the price of additional + * runtime processing overhead. + *
+ * + *

Note that it is also possible to specify a + * {@link org.springframework.jms.connection.JmsTransactionManager} as external + * "transactionManager", providing fully synchronized Spring transactions based + * on local JMS transactions. The effect is similar to "sessionTransacted" set + * to "true", the difference being that this external transaction management + * will also affect independent JMS access code within the service layer + * (e.g. based on {@link org.springframework.jms.core.JmsTemplate} or + * {@link org.springframework.jms.connection.TransactionAwareConnectionFactoryProxy}), + * not just direct JMS Session usage in a {@link SessionAwareMessageListener}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setMessageListener + * @see javax.jms.MessageListener + * @see SessionAwareMessageListener + * @see #handleListenerException + * @see DefaultMessageListenerContainer + * @see SimpleMessageListenerContainer + * @see org.springframework.jms.listener.endpoint.JmsMessageEndpointManager + */ +public abstract class AbstractMessageListenerContainer extends AbstractJmsListeningContainer { + + private volatile Object destination; + + private volatile String messageSelector; + + private volatile Object messageListener; + + private boolean subscriptionDurable = false; + + private String durableSubscriptionName; + + private ExceptionListener exceptionListener; + + private boolean exposeListenerSession = true; + + private boolean acceptMessagesWhileStopping = false; + + + /** + * Set the destination to receive messages from. + *

Alternatively, specify a "destinationName", to be dynamically + * resolved via the {@link org.springframework.jms.support.destination.DestinationResolver}. + *

Note: The destination may be replaced at runtime, with the listener + * container picking up the new destination immediately (works e.g. with + * DefaultMessageListenerContainer, as long as the cache level is less than + * CACHE_CONSUMER). However, this is considered advanced usage; use it with care! + * @see #setDestinationName(String) + */ + public void setDestination(Destination destination) { + Assert.notNull(destination, "'destination' must not be null"); + this.destination = destination; + if (destination instanceof Topic && !(destination instanceof Queue)) { + // Clearly a Topic: let's set the "pubSubDomain" flag accordingly. + setPubSubDomain(true); + } + } + + /** + * Return the destination to receive messages from. Will be null + * if the configured destination is not an actual {@link Destination} type; + * c.f. {@link #setDestinationName(String) when the destination is a String}. + */ + public Destination getDestination() { + return (this.destination instanceof Destination ? (Destination) this.destination : null); + } + + /** + * Set the name of the destination to receive messages from. + *

The specified name will be dynamically resolved via the configured + * {@link #setDestinationResolver destination resolver}. + *

Alternatively, specify a JMS {@link Destination} object as "destination". + *

Note: The destination may be replaced at runtime, with the listener + * container picking up the new destination immediately (works e.g. with + * DefaultMessageListenerContainer, as long as the cache level is less than + * CACHE_CONSUMER). However, this is considered advanced usage; use it with care! + * @param destinationName the desired destination (can be null) + * @see #setDestination(javax.jms.Destination) + */ + public void setDestinationName(String destinationName) { + Assert.notNull(destinationName, "'destinationName' must not be null"); + this.destination = destinationName; + } + + /** + * Return the name of the destination to receive messages from. + * Will be null if the configured destination is not a + * {@link String} type; c.f. {@link #setDestination(Destination) when + * it is an actual Destination}. + */ + public String getDestinationName() { + return (this.destination instanceof String ? (String) this.destination : null); + } + + /** + * Return a descriptive String for this container's JMS destination + * (never null). + */ + protected String getDestinationDescription() { + return this.destination.toString(); + } + + /** + * Set the JMS message selector expression (or null if none). + * Default is none. + *

See the JMS specification for a detailed definition of selector expressions. + *

Note: The message selector may be replaced at runtime, with the listener + * container picking up the new selector value immediately (works e.g. with + * DefaultMessageListenerContainer, as long as the cache level is less than + * CACHE_CONSUMER). However, this is considered advanced usage; use it with care! + */ + public void setMessageSelector(String messageSelector) { + this.messageSelector = messageSelector; + } + + /** + * Return the JMS message selector expression (or null if none). + */ + public String getMessageSelector() { + return this.messageSelector; + } + + + /** + * Set the message listener implementation to register. + * This can be either a standard JMS {@link MessageListener} object + * or a Spring {@link SessionAwareMessageListener} object. + *

Note: The message listener may be replaced at runtime, with the listener + * container picking up the new listener object immediately (works e.g. with + * DefaultMessageListenerContainer, as long as the cache level is less than + * CACHE_CONSUMER). However, this is considered advanced usage; use it with care! + * @throws IllegalArgumentException if the supplied listener is not a + * {@link MessageListener} or a {@link SessionAwareMessageListener} + * @see javax.jms.MessageListener + * @see SessionAwareMessageListener + */ + public void setMessageListener(Object messageListener) { + checkMessageListener(messageListener); + this.messageListener = messageListener; + if (this.durableSubscriptionName == null) { + this.durableSubscriptionName = getDefaultSubscriptionName(messageListener); + } + } + + /** + * Return the message listener object to register. + */ + public Object getMessageListener() { + return this.messageListener; + } + + /** + * Check the given message listener, throwing an exception + * if it does not correspond to a supported listener type. + *

By default, only a standard JMS {@link MessageListener} object or a + * Spring {@link SessionAwareMessageListener} object will be accepted. + * @param messageListener the message listener object to check + * @throws IllegalArgumentException if the supplied listener is not a + * {@link MessageListener} or a {@link SessionAwareMessageListener} + * @see javax.jms.MessageListener + * @see SessionAwareMessageListener + */ + protected void checkMessageListener(Object messageListener) { + if (!(messageListener instanceof MessageListener || + messageListener instanceof SessionAwareMessageListener)) { + throw new IllegalArgumentException( + "Message listener needs to be of type [" + MessageListener.class.getName() + + "] or [" + SessionAwareMessageListener.class.getName() + "]"); + } + } + + /** + * Determine the default subscription name for the given message listener. + * @param messageListener the message listener object to check + * @return the default subscription name + * @see SubscriptionNameProvider + */ + protected String getDefaultSubscriptionName(Object messageListener) { + if (messageListener instanceof SubscriptionNameProvider) { + return ((SubscriptionNameProvider) messageListener).getSubscriptionName(); + } + else { + return messageListener.getClass().getName(); + } + } + + /** + * Set whether to make the subscription durable. The durable subscription name + * to be used can be specified through the "durableSubscriptionName" property. + *

Default is "false". Set this to "true" to register a durable subscription, + * typically in combination with a "durableSubscriptionName" value (unless + * your message listener class name is good enough as subscription name). + *

Only makes sense when listening to a topic (pub-sub domain). + * @see #setDurableSubscriptionName + */ + public void setSubscriptionDurable(boolean subscriptionDurable) { + this.subscriptionDurable = subscriptionDurable; + } + + /** + * Return whether to make the subscription durable. + */ + public boolean isSubscriptionDurable() { + return this.subscriptionDurable; + } + + /** + * Set the name of a durable subscription to create. To be applied in case + * of a topic (pub-sub domain) with subscription durability activated. + *

The durable subscription name needs to be unique within this client's + * JMS client id. Default is the class name of the specified message listener. + *

Note: Only 1 concurrent consumer (which is the default of this + * message listener container) is allowed for each durable subscription. + * @see #setSubscriptionDurable + * @see #setClientId + * @see #setMessageListener + */ + public void setDurableSubscriptionName(String durableSubscriptionName) { + this.durableSubscriptionName = durableSubscriptionName; + } + + /** + * Return the name of a durable subscription to create, if any. + */ + public String getDurableSubscriptionName() { + return this.durableSubscriptionName; + } + + /** + * Set the JMS ExceptionListener to notify in case of a JMSException thrown + * by the registered message listener or the invocation infrastructure. + */ + public void setExceptionListener(ExceptionListener exceptionListener) { + this.exceptionListener = exceptionListener; + } + + /** + * Return the JMS ExceptionListener to notify in case of a JMSException thrown + * by the registered message listener or the invocation infrastructure, if any. + */ + public ExceptionListener getExceptionListener() { + return this.exceptionListener; + } + + /** + * Set whether to expose the listener JMS Session to a registered + * {@link SessionAwareMessageListener} as well as to + * {@link org.springframework.jms.core.JmsTemplate} calls. + *

Default is "true", reusing the listener's {@link Session}. + * Turn this off to expose a fresh JMS Session fetched from the same + * underlying JMS {@link Connection} instead, which might be necessary + * on some JMS providers. + *

Note that Sessions managed by an external transaction manager will + * always get exposed to {@link org.springframework.jms.core.JmsTemplate} + * calls. So in terms of JmsTemplate exposure, this setting only affects + * locally transacted Sessions. + * @see SessionAwareMessageListener + */ + public void setExposeListenerSession(boolean exposeListenerSession) { + this.exposeListenerSession = exposeListenerSession; + } + + /** + * Return whether to expose the listener JMS {@link Session} to a + * registered {@link SessionAwareMessageListener}. + */ + public boolean isExposeListenerSession() { + return this.exposeListenerSession; + } + + /** + * Set whether to accept received messages while the listener container + * in the process of stopping. + *

Default is "false", rejecting such messages through aborting the + * receive attempt. Switch this flag on to fully process such messages + * even in the stopping phase, with the drawback that even newly sent + * messages might still get processed (if coming in before all receive + * timeouts have expired). + *

NOTE: Aborting receive attempts for such incoming messages + * might lead to the provider's retry count decreasing for the affected + * messages. If you have a high number of concurrent consumers, make sure + * that the number of retries is higher than the number of consumers, + * to be on the safe side for all potential stopping scenarios. + */ + public void setAcceptMessagesWhileStopping(boolean acceptMessagesWhileStopping) { + this.acceptMessagesWhileStopping = acceptMessagesWhileStopping; + } + + /** + * Return whether to accept received messages while the listener container + * in the process of stopping. + */ + public boolean isAcceptMessagesWhileStopping() { + return this.acceptMessagesWhileStopping; + } + + protected void validateConfiguration() { + if (this.destination == null) { + throw new IllegalArgumentException("Property 'destination' or 'destinationName' is required"); + } + if (isSubscriptionDurable() && !isPubSubDomain()) { + throw new IllegalArgumentException("A durable subscription requires a topic (pub-sub domain)"); + } + } + + + //------------------------------------------------------------------------- + // Template methods for listener execution + //------------------------------------------------------------------------- + + /** + * Execute the specified listener, + * committing or rolling back the transaction afterwards (if necessary). + * @param session the JMS Session to operate on + * @param message the received JMS Message + * @see #invokeListener + * @see #commitIfNecessary + * @see #rollbackOnExceptionIfNecessary + * @see #handleListenerException + */ + protected void executeListener(Session session, Message message) { + try { + doExecuteListener(session, message); + } + catch (Throwable ex) { + handleListenerException(ex); + } + } + + /** + * Execute the specified listener, + * committing or rolling back the transaction afterwards (if necessary). + * @param session the JMS Session to operate on + * @param message the received JMS Message + * @throws JMSException if thrown by JMS API methods + * @see #invokeListener + * @see #commitIfNecessary + * @see #rollbackOnExceptionIfNecessary + * @see #convertJmsAccessException + */ + protected void doExecuteListener(Session session, Message message) throws JMSException { + if (!isAcceptMessagesWhileStopping() && !isRunning()) { + if (logger.isWarnEnabled()) { + logger.warn("Rejecting received message because of the listener container " + + "having been stopped in the meantime: " + message); + } + rollbackIfNecessary(session); + throw new MessageRejectedWhileStoppingException(); + } + try { + invokeListener(session, message); + } + catch (JMSException ex) { + rollbackOnExceptionIfNecessary(session, ex); + throw ex; + } + catch (RuntimeException ex) { + rollbackOnExceptionIfNecessary(session, ex); + throw ex; + } + catch (Error err) { + rollbackOnExceptionIfNecessary(session, err); + throw err; + } + commitIfNecessary(session, message); + } + + /** + * Invoke the specified listener: either as standard JMS MessageListener + * or (preferably) as Spring SessionAwareMessageListener. + * @param session the JMS Session to operate on + * @param message the received JMS Message + * @throws JMSException if thrown by JMS API methods + * @see #setMessageListener + */ + protected void invokeListener(Session session, Message message) throws JMSException { + Object listener = getMessageListener(); + if (listener instanceof SessionAwareMessageListener) { + doInvokeListener((SessionAwareMessageListener) listener, session, message); + } + else if (listener instanceof MessageListener) { + doInvokeListener((MessageListener) listener, message); + } + else if (listener != null) { + throw new IllegalArgumentException( + "Only MessageListener and SessionAwareMessageListener supported: " + listener); + } + else { + throw new IllegalStateException("No message listener specified - see property 'messageListener'"); + } + } + + /** + * Invoke the specified listener as Spring SessionAwareMessageListener, + * exposing a new JMS Session (potentially with its own transaction) + * to the listener if demanded. + * @param listener the Spring SessionAwareMessageListener to invoke + * @param session the JMS Session to operate on + * @param message the received JMS Message + * @throws JMSException if thrown by JMS API methods + * @see SessionAwareMessageListener + * @see #setExposeListenerSession + */ + protected void doInvokeListener(SessionAwareMessageListener listener, Session session, Message message) + throws JMSException { + + Connection conToClose = null; + Session sessionToClose = null; + try { + Session sessionToUse = session; + if (!isExposeListenerSession()) { + // We need to expose a separate Session. + conToClose = createConnection(); + sessionToClose = createSession(conToClose); + sessionToUse = sessionToClose; + } + // Actually invoke the message listener... + listener.onMessage(message, sessionToUse); + // Clean up specially exposed Session, if any. + if (sessionToUse != session) { + if (sessionToUse.getTransacted() && isSessionLocallyTransacted(sessionToUse)) { + // Transacted session created by this container -> commit. + JmsUtils.commitIfNecessary(sessionToUse); + } + } + } + finally { + JmsUtils.closeSession(sessionToClose); + JmsUtils.closeConnection(conToClose); + } + } + + /** + * Invoke the specified listener as standard JMS MessageListener. + *

Default implementation performs a plain invocation of the + * onMessage method. + * @param listener the JMS MessageListener to invoke + * @param message the received JMS Message + * @throws JMSException if thrown by JMS API methods + * @see javax.jms.MessageListener#onMessage + */ + protected void doInvokeListener(MessageListener listener, Message message) throws JMSException { + listener.onMessage(message); + } + + /** + * Perform a commit or message acknowledgement, as appropriate. + * @param session the JMS Session to commit + * @param message the Message to acknowledge + * @throws javax.jms.JMSException in case of commit failure + */ + protected void commitIfNecessary(Session session, Message message) throws JMSException { + // Commit session or acknowledge message. + if (session.getTransacted()) { + // Commit necessary - but avoid commit call within a JTA transaction. + if (isSessionLocallyTransacted(session)) { + // Transacted session created by this container -> commit. + JmsUtils.commitIfNecessary(session); + } + } + else if (isClientAcknowledge(session)) { + message.acknowledge(); + } + } + + /** + * Perform a rollback, if appropriate. + * @param session the JMS Session to rollback + * @throws javax.jms.JMSException in case of a rollback error + */ + protected void rollbackIfNecessary(Session session) throws JMSException { + if (session.getTransacted() && isSessionLocallyTransacted(session)) { + // Transacted session created by this container -> rollback. + JmsUtils.rollbackIfNecessary(session); + } + } + + /** + * Perform a rollback, handling rollback exceptions properly. + * @param session the JMS Session to rollback + * @param ex the thrown application exception or error + * @throws javax.jms.JMSException in case of a rollback error + */ + protected void rollbackOnExceptionIfNecessary(Session session, Throwable ex) throws JMSException { + try { + if (session.getTransacted() && isSessionLocallyTransacted(session)) { + // Transacted session created by this container -> rollback. + if (logger.isDebugEnabled()) { + logger.debug("Initiating transaction rollback on application exception", ex); + } + JmsUtils.rollbackIfNecessary(session); + } + } + catch (IllegalStateException ex2) { + logger.debug("Could not roll back because Session already closed", ex2); + } + catch (JMSException ex2) { + logger.error("Application exception overridden by rollback exception", ex); + throw ex2; + } + catch (RuntimeException ex2) { + logger.error("Application exception overridden by rollback exception", ex); + throw ex2; + } + catch (Error err) { + logger.error("Application exception overridden by rollback error", ex); + throw err; + } + } + + /** + * Check whether the given Session is locally transacted, that is, whether + * its transaction is managed by this listener container's Session handling + * and not by an external transaction coordinator. + *

Note: The Session's own transacted flag will already have been checked + * before. This method is about finding out whether the Session's transaction + * is local or externally coordinated. + * @param session the Session to check + * @return whether the given Session is locally transacted + * @see #isSessionTransacted() + * @see org.springframework.jms.connection.ConnectionFactoryUtils#isSessionTransactional + */ + protected boolean isSessionLocallyTransacted(Session session) { + return isSessionTransacted(); + } + + /** + * Handle the given exception that arose during listener execution. + *

The default implementation logs the exception at error level, + * not propagating it to the JMS provider - assuming that all handling of + * acknowledgement and/or transactions is done by this listener container. + * This can be overridden in subclasses. + * @param ex the exception to handle + */ + protected void handleListenerException(Throwable ex) { + if (ex instanceof MessageRejectedWhileStoppingException) { + // Internal exception - has been handled before. + return; + } + if (ex instanceof JMSException) { + invokeExceptionListener((JMSException) ex); + } + if (isActive()) { + // Regular case: failed while active. + // Log at error level. + logger.warn("Execution of JMS message listener failed", ex); + } + else { + // Rare case: listener thread failed after container shutdown. + // Log at debug level, to avoid spamming the shutdown log. + logger.debug("Listener exception after container shutdown", ex); + } + } + + /** + * Invoke the registered JMS ExceptionListener, if any. + * @param ex the exception that arose during JMS processing + * @see #setExceptionListener + */ + protected void invokeExceptionListener(JMSException ex) { + ExceptionListener exceptionListener = getExceptionListener(); + if (exceptionListener != null) { + exceptionListener.onException(ex); + } + } + + + /** + * Internal exception class that indicates a rejected message on shutdown. + * Used to trigger a rollback for an external transaction manager in that case. + */ + private static class MessageRejectedWhileStoppingException extends RuntimeException { + + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java new file mode 100644 index 00000000000..376d6a917b3 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java @@ -0,0 +1,514 @@ +/* + * Copyright 2002-2008 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 javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.Session; +import javax.jms.Topic; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.jms.connection.ConnectionFactoryUtils; +import org.springframework.jms.connection.JmsResourceHolder; +import org.springframework.jms.connection.SingleConnectionFactory; +import org.springframework.jms.support.JmsUtils; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.springframework.transaction.support.ResourceTransactionManager; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionSynchronizationUtils; + +/** + * Base class for listener container implementations which are based on polling. + * Provides support for listener handling based on {@link javax.jms.MessageConsumer}, + * optionally participating in externally managed transactions. + * + *

This listener container variant is built for repeated polling attempts, + * each invoking the {@link #receiveAndExecute} method. The MessageConsumer used + * may be reobtained fo reach attempt or cached inbetween attempts; this is up + * to the concrete implementation. The receive timeout for each attempt can be + * configured through the {@link #setReceiveTimeout "receiveTimeout"} property. + * + *

The underlying mechanism is based on standard JMS MessageConsumer handling, + * which is perfectly compatible with both native JMS and JMS in a J2EE environment. + * Neither the JMS MessageConsumer.setMessageListener facility + * nor the JMS ServerSessionPool facility is required. A further advantage + * of this approach is full control over the listening process, allowing for + * custom scaling and throttling and of concurrent message processing + * (which is up to concrete subclasses). + * + *

Message reception and listener execution can automatically be wrapped + * in transactions through passing a Spring + * {@link org.springframework.transaction.PlatformTransactionManager} into the + * {@link #setTransactionManager "transactionManager"} property. This will usually + * be a {@link org.springframework.transaction.jta.JtaTransactionManager} in a + * J2EE enviroment, in combination with a JTA-aware JMS ConnectionFactory obtained + * from JNDI (check your J2EE server's documentation). + * + *

This base class does not assume any specific mechanism for asynchronous + * execution of polling invokers. Check out {@link DefaultMessageListenerContainer} + * for a concrete implementation which is based on Spring's + * {@link org.springframework.core.task.TaskExecutor} abstraction, + * including dynamic scaling of concurrent consumers and automatic self recovery. + * + * @author Juergen Hoeller + * @since 2.0.3 + * @see #createListenerConsumer + * @see #receiveAndExecute + * @see #setTransactionManager + */ +public abstract class AbstractPollingMessageListenerContainer extends AbstractMessageListenerContainer + implements BeanNameAware { + + /** + * The default receive timeout: 1000 ms = 1 second. + */ + public static final long DEFAULT_RECEIVE_TIMEOUT = 1000; + + + private final MessageListenerContainerResourceFactory transactionalResourceFactory = + new MessageListenerContainerResourceFactory(); + + private boolean sessionTransactedCalled = false; + + private boolean pubSubNoLocal = false; + + private PlatformTransactionManager transactionManager; + + private DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); + + private long receiveTimeout = DEFAULT_RECEIVE_TIMEOUT; + + + public void setSessionTransacted(boolean sessionTransacted) { + super.setSessionTransacted(sessionTransacted); + this.sessionTransactedCalled = true; + } + + /** + * Set whether to inhibit the delivery of messages published by its own connection. + * Default is "false". + * @see javax.jms.TopicSession#createSubscriber(javax.jms.Topic, String, boolean) + */ + public void setPubSubNoLocal(boolean pubSubNoLocal) { + this.pubSubNoLocal = pubSubNoLocal; + } + + /** + * Return whether to inhibit the delivery of messages published by its own connection. + */ + protected boolean isPubSubNoLocal() { + return this.pubSubNoLocal; + } + + /** + * Specify the Spring {@link org.springframework.transaction.PlatformTransactionManager} + * to use for transactional wrapping of message reception plus listener execution. + *

Default is none, not performing any transactional wrapping. + * If specified, this will usually be a Spring + * {@link org.springframework.transaction.jta.JtaTransactionManager} or one + * of its subclasses, in combination with a JTA-aware ConnectionFactory that + * this message listener container obtains its Connections from. + *

Note: Consider the use of local JMS transactions instead. + * Simply switch the {@link #setSessionTransacted "sessionTransacted"} flag + * to "true" in order to use a locally transacted JMS Session for the entire + * receive processing, including any Session operations performed by a + * {@link SessionAwareMessageListener} (e.g. sending a response message). + * Alternatively, a {@link org.springframework.jms.connection.JmsTransactionManager} + * may be used for fully synchronized Spring transactions based on local JMS + * transactions. Check {@link AbstractMessageListenerContainer}'s javadoc for + * a discussion of transaction choices and message redelivery scenarios. + * @see org.springframework.transaction.jta.JtaTransactionManager + * @see org.springframework.jms.connection.JmsTransactionManager + */ + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + /** + * Return the Spring PlatformTransactionManager to use for transactional + * wrapping of message reception plus listener execution. + */ + protected final PlatformTransactionManager getTransactionManager() { + return this.transactionManager; + } + + /** + * Specify the transaction name to use for transactional wrapping. + * Default is the bean name of this listener container, if any. + * @see org.springframework.transaction.TransactionDefinition#getName() + */ + public void setTransactionName(String transactionName) { + this.transactionDefinition.setName(transactionName); + } + + /** + * Specify the transaction timeout to use for transactional wrapping, in seconds. + * Default is none, using the transaction manager's default timeout. + * @see org.springframework.transaction.TransactionDefinition#getTimeout() + * @see #setReceiveTimeout + */ + public void setTransactionTimeout(int transactionTimeout) { + this.transactionDefinition.setTimeout(transactionTimeout); + } + + /** + * Set the timeout to use for receive calls, in milliseconds. + * The default is 1000 ms, that is, 1 second. + *

NOTE: This value needs to be smaller than the transaction + * timeout used by the transaction manager (in the appropriate unit, + * of course). -1 indicates no timeout at all; however, this is only + * feasible if not running within a transaction manager. + * @see javax.jms.MessageConsumer#receive(long) + * @see javax.jms.MessageConsumer#receive() + * @see #setTransactionTimeout + */ + public void setReceiveTimeout(long receiveTimeout) { + this.receiveTimeout = receiveTimeout; + } + + + public void initialize() { + // Set sessionTransacted=true in case of a non-JTA transaction manager. + if (!this.sessionTransactedCalled && + this.transactionManager instanceof ResourceTransactionManager && + !TransactionSynchronizationUtils.sameResourceFactory( + (ResourceTransactionManager) this.transactionManager, getConnectionFactory())) { + super.setSessionTransacted(true); + } + + // Use bean name as default transaction name. + if (this.transactionDefinition.getName() == null) { + this.transactionDefinition.setName(getBeanName()); + } + + // Proceed with superclass initialization. + super.initialize(); + } + + + /** + * Create a MessageConsumer for the given JMS Session, + * registering a MessageListener for the specified listener. + * @param session the JMS Session to work on + * @return the MessageConsumer + * @throws javax.jms.JMSException if thrown by JMS methods + * @see #receiveAndExecute + */ + protected MessageConsumer createListenerConsumer(Session session) throws JMSException { + Destination destination = getDestination(); + if (destination == null) { + destination = resolveDestinationName(session, getDestinationName()); + } + return createConsumer(session, destination); + } + + /** + * Execute the listener for a message received from the given consumer, + * wrapping the entire operation in an external transaction if demanded. + * @param session the JMS Session to work on + * @param consumer the MessageConsumer to work on + * @return whether a message has been received + * @throws JMSException if thrown by JMS methods + * @see #doReceiveAndExecute + */ + protected boolean receiveAndExecute(Object invoker, Session session, MessageConsumer consumer) + throws JMSException { + + if (this.transactionManager != null) { + // Execute receive within transaction. + TransactionStatus status = this.transactionManager.getTransaction(this.transactionDefinition); + boolean messageReceived = true; + try { + messageReceived = doReceiveAndExecute(invoker, session, consumer, status); + } + catch (JMSException ex) { + rollbackOnException(status, ex); + throw ex; + } + catch (RuntimeException ex) { + rollbackOnException(status, ex); + throw ex; + } + catch (Error err) { + rollbackOnException(status, err); + throw err; + } + this.transactionManager.commit(status); + return messageReceived; + } + + else { + // Execute receive outside of transaction. + return doReceiveAndExecute(invoker, session, consumer, null); + } + } + + /** + * Actually execute the listener for a message received from the given consumer, + * fetching all requires resources and invoking the listener. + * @param session the JMS Session to work on + * @param consumer the MessageConsumer to work on + * @param status the TransactionStatus (may be null) + * @return whether a message has been received + * @throws JMSException if thrown by JMS methods + * @see #doExecuteListener(javax.jms.Session, javax.jms.Message) + */ + protected boolean doReceiveAndExecute( + Object invoker, Session session, MessageConsumer consumer, TransactionStatus status) + throws JMSException { + + Connection conToClose = null; + Session sessionToClose = null; + MessageConsumer consumerToClose = null; + try { + Session sessionToUse = session; + boolean transactional = false; + if (sessionToUse == null) { + sessionToUse = ConnectionFactoryUtils.doGetTransactionalSession( + getConnectionFactory(), this.transactionalResourceFactory, true); + transactional = (sessionToUse != null); + } + if (sessionToUse == null) { + Connection conToUse = null; + if (sharedConnectionEnabled()) { + conToUse = getSharedConnection(); + } + else { + conToUse = createConnection(); + conToClose = conToUse; + conToUse.start(); + } + sessionToUse = createSession(conToUse); + sessionToClose = sessionToUse; + } + MessageConsumer consumerToUse = consumer; + if (consumerToUse == null) { + consumerToUse = createListenerConsumer(sessionToUse); + consumerToClose = consumerToUse; + } + Message message = receiveMessage(consumerToUse); + if (message != null) { + if (logger.isDebugEnabled()) { + logger.debug("Received message of type [" + message.getClass() + "] from consumer [" + + consumerToUse + "] of " + (transactional ? "transactional " : "") + "session [" + + sessionToUse + "]"); + } + messageReceived(invoker, sessionToUse); + boolean exposeResource = (!transactional && isExposeListenerSession() && + !TransactionSynchronizationManager.hasResource(getConnectionFactory())); + if (exposeResource) { + TransactionSynchronizationManager.bindResource( + getConnectionFactory(), new LocallyExposedJmsResourceHolder(sessionToUse)); + } + try { + doExecuteListener(sessionToUse, message); + } + catch (Throwable ex) { + if (status != null) { + if (logger.isDebugEnabled()) { + logger.debug("Rolling back transaction because of listener exception thrown: " + ex); + } + status.setRollbackOnly(); + } + handleListenerException(ex); + // Rethrow JMSException to indicate an infrastructure problem + // that may have to trigger recovery... + if (ex instanceof JMSException) { + throw (JMSException) ex; + } + } + finally { + if (exposeResource) { + TransactionSynchronizationManager.unbindResource(getConnectionFactory()); + } + } + return true; + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Consumer [" + consumerToUse + "] of " + (transactional ? "transactional " : "") + + "session [" + sessionToUse + "] did not receive a message"); + } + noMessageReceived(invoker, sessionToUse); + return false; + } + } + finally { + JmsUtils.closeMessageConsumer(consumerToClose); + JmsUtils.closeSession(sessionToClose); + ConnectionFactoryUtils.releaseConnection(conToClose, getConnectionFactory(), true); + } + } + + /** + * This implementation checks whether the Session is externally synchronized. + * In this case, the Session is not locally transacted, despite the listener + * container's "sessionTransacted" flag being set to "true". + * @see org.springframework.jms.connection.JmsResourceHolder + */ + protected boolean isSessionLocallyTransacted(Session session) { + if (!super.isSessionLocallyTransacted(session)) { + return false; + } + JmsResourceHolder resourceHolder = + (JmsResourceHolder) TransactionSynchronizationManager.getResource(getConnectionFactory()); + return (resourceHolder == null || resourceHolder instanceof LocallyExposedJmsResourceHolder || + !resourceHolder.containsSession(session)); + } + + /** + * Perform a rollback, handling rollback exceptions properly. + * @param status object representing the transaction + * @param ex the thrown listener exception or error + */ + private void rollbackOnException(TransactionStatus status, Throwable ex) { + logger.debug("Initiating transaction rollback on listener exception", ex); + try { + this.transactionManager.rollback(status); + } + catch (RuntimeException ex2) { + logger.error("Listener exception overridden by rollback exception", ex); + throw ex2; + } + catch (Error err) { + logger.error("Listener exception overridden by rollback error", ex); + throw err; + } + } + + /** + * Receive a message from the given consumer. + * @param consumer the MessageConsumer to use + * @return the Message, or null if none + * @throws JMSException if thrown by JMS methods + */ + protected Message receiveMessage(MessageConsumer consumer) throws JMSException { + return (this.receiveTimeout < 0 ? consumer.receive() : consumer.receive(this.receiveTimeout)); + } + + /** + * Template method that gets called right when a new message has been received, + * before attempting to process it. Allows subclasses to react to the event + * of an actual incoming message, for example adapting their consumer count. + * @param invoker the invoker object (passed through) + * @param session the receiving JMS Session + */ + protected void messageReceived(Object invoker, Session session) { + } + + /** + * Template method that gets called right no message has been received, + * before attempting to process it. Allows subclasses to react to the event + * of an actual incoming message, for example marking . + * @param invoker the invoker object (passed through) + * @param session the receiving JMS Session + */ + protected void noMessageReceived(Object invoker, Session session) { + } + + + //------------------------------------------------------------------------- + // JMS 1.1 factory methods, potentially overridden for JMS 1.0.2 + //------------------------------------------------------------------------- + + /** + * Fetch an appropriate Connection from the given JmsResourceHolder. + *

This implementation accepts any JMS 1.1 Connection. + * @param holder the JmsResourceHolder + * @return an appropriate Connection fetched from the holder, + * or null if none found + */ + protected Connection getConnection(JmsResourceHolder holder) { + return holder.getConnection(); + } + + /** + * Fetch an appropriate Session from the given JmsResourceHolder. + *

This implementation accepts any JMS 1.1 Session. + * @param holder the JmsResourceHolder + * @return an appropriate Session fetched from the holder, + * or null if none found + */ + protected Session getSession(JmsResourceHolder holder) { + return holder.getSession(); + } + + /** + * Create a JMS MessageConsumer for the given Session and Destination. + *

This implementation uses JMS 1.1 API. + * @param session the JMS Session to create a MessageConsumer for + * @param destination the JMS Destination to create a MessageConsumer for + * @return the new JMS MessageConsumer + * @throws javax.jms.JMSException if thrown by JMS API methods + */ + protected MessageConsumer createConsumer(Session session, Destination destination) throws JMSException { + // Only pass in the NoLocal flag in case of a Topic: + // Some JMS providers, such as WebSphere MQ 6.0, throw IllegalStateException + // in case of the NoLocal flag being specified for a Queue. + if (isPubSubDomain()) { + if (isSubscriptionDurable() && destination instanceof Topic) { + return session.createDurableSubscriber( + (Topic) destination, getDurableSubscriptionName(), getMessageSelector(), isPubSubNoLocal()); + } + else { + return session.createConsumer(destination, getMessageSelector(), isPubSubNoLocal()); + } + } + else { + return session.createConsumer(destination, getMessageSelector()); + } + } + + + /** + * ResourceFactory implementation that delegates to this listener container's protected callback methods. + */ + private class MessageListenerContainerResourceFactory implements ConnectionFactoryUtils.ResourceFactory { + + public Connection getConnection(JmsResourceHolder holder) { + return AbstractPollingMessageListenerContainer.this.getConnection(holder); + } + + public Session getSession(JmsResourceHolder holder) { + return AbstractPollingMessageListenerContainer.this.getSession(holder); + } + + public Connection createConnection() throws JMSException { + if (AbstractPollingMessageListenerContainer.this.sharedConnectionEnabled()) { + Connection sharedCon = AbstractPollingMessageListenerContainer.this.getSharedConnection(); + return new SingleConnectionFactory(sharedCon).createConnection(); + } + else { + return AbstractPollingMessageListenerContainer.this.createConnection(); + } + } + + public Session createSession(Connection con) throws JMSException { + return AbstractPollingMessageListenerContainer.this.createSession(con); + } + + public boolean isSynchedLocalTransactionAllowed() { + return AbstractPollingMessageListenerContainer.this.isSessionTransacted(); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java new file mode 100644 index 00000000000..be3851db4a2 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java @@ -0,0 +1,1044 @@ +/* + * Copyright 2002-2008 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 java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.MessageConsumer; +import javax.jms.Session; + +import org.springframework.core.Constants; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.jms.JmsException; +import org.springframework.jms.support.JmsUtils; +import org.springframework.jms.support.destination.CachingDestinationResolver; +import org.springframework.jms.support.destination.DestinationResolver; +import org.springframework.scheduling.SchedulingAwareRunnable; +import org.springframework.scheduling.SchedulingTaskExecutor; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Message listener container variant that uses plain JMS client API, specifically + * a loop of MessageConsumer.receive() calls that also allow for + * transactional reception of messages (registering them with XA transactions). + * Designed to work in a native JMS environment as well as in a J2EE environment, + * with only minimal differences in configuration. + * + *

NOTE: This class requires a JMS 1.1+ provider, because it builds on + * the domain-independent API. Use the {@link DefaultMessageListenerContainer102} + * subclass for a JMS 1.0.2 provider, e.g. when running on a J2EE 1.3 server. + * + *

This is a simple but nevertheless powerful form of message listener container. + * On startup, it obtains a fixed number of JMS Sessions to invoke the listener, + * and optionally allows for dynamic adaptation at runtime (up until a maximum number). + * Like {@link SimpleMessageListenerContainer}, its main advantage is its low level + * of runtime complexity, in particular the minimal requirements on the JMS provider: + * Not even the JMS ServerSessionPool facility is required. Beyond that, it is + * fully self-recovering in case of the broker being temporarily unavailable, + * and allows for stops/restarts as well as runtime changes to its configuration. + * + *

Actual MessageListener execution happens in asynchronous work units which are + * created through Spring's {@link org.springframework.core.task.TaskExecutor} + * abstraction. By default, the specified number of invoker tasks will be created + * on startup, according to the {@link #setConcurrentConsumers "concurrentConsumers"} + * setting. Specify an alternative TaskExecutor to integrate with an existing + * thread pool facility (such as a J2EE server's), for example using a + * {@link org.springframework.scheduling.commonj.WorkManagerTaskExecutor CommonJ WorkManager}. + * With a native JMS setup, each of those listener threads is going to use a + * cached JMS Session and MessageConsumer (only refreshed in case of failure), + * using the JMS provider's resources as efficiently as possible. + * + *

Message reception and listener execution can automatically be wrapped + * in transactions through passing a Spring + * {@link org.springframework.transaction.PlatformTransactionManager} into the + * {@link #setTransactionManager "transactionManager"} property. This will usually + * be a {@link org.springframework.transaction.jta.JtaTransactionManager} in a + * J2EE enviroment, in combination with a JTA-aware JMS ConnectionFactory obtained + * from JNDI (check your J2EE server's documentation). Note that this listener + * container will automatically reobtain all JMS handles for each transaction + * in case of an external transaction manager specified, for compatibility with + * all J2EE servers (in particular JBoss). This non-caching behavior can be + * overridden through the {@link #setCacheLevel "cacheLevel"} / + * {@link #setCacheLevelName "cacheLevelName"} property, enforcing caching + * of the Connection (or also Session and MessageConsumer) even in case of + * an external transaction manager being involved. + * + *

Dynamic scaling of the number of concurrent invokers can be activated + * through specifying a {@link #setMaxConcurrentConsumers "maxConcurrentConsumers"} + * value that is higher than the {@link #setConcurrentConsumers "concurrentConsumers"} + * value. Since the latter's default is 1, you can also simply specify a + * "maxConcurrentConsumers" of e.g. 5, which will lead to dynamic scaling up to + * 5 concurrent consumers in case of increasing message load, as well as dynamic + * shrinking back to the standard number of consumers once the load decreases. + * Consider adapting the {@link #setIdleTaskExecutionLimit "idleTaskExecutionLimit"} + * setting to control the lifespan of each new task, to avoid frequent scaling up + * and down, in particular if the ConnectionFactory does not pool JMS Sessions + * and/or the TaskExecutor does not pool threads (check your configuration!). + * Note that dynamic scaling only really makes sense for a queue in the first + * place; for a topic, you will typically stick with the default number of 1 + * consumer, else you'd receive the same message multiple times on the same node. + * + *

It is strongly recommended to either set {@link #setSessionTransacted + * "sessionTransacted"} to "true" or specify an external {@link #setTransactionManager + * "transactionManager"}. See the {@link AbstractMessageListenerContainer} + * javadoc for details on acknowledge modes and native transaction options, + * as well as the {@link AbstractPollingMessageListenerContainer} javadoc + * for details on configuring an external transaction manager. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setTransactionManager + * @see #setCacheLevel + * @see javax.jms.MessageConsumer#receive(long) + * @see SimpleMessageListenerContainer + * @see org.springframework.jms.listener.endpoint.JmsMessageEndpointManager + */ +public class DefaultMessageListenerContainer extends AbstractPollingMessageListenerContainer { + + /** + * Default thread name prefix: "DefaultMessageListenerContainer-". + */ + public static final String DEFAULT_THREAD_NAME_PREFIX = + ClassUtils.getShortName(DefaultMessageListenerContainer.class) + "-"; + + /** + * The default recovery interval: 5000 ms = 5 seconds. + */ + public static final long DEFAULT_RECOVERY_INTERVAL = 5000; + + + /** + * Constant that indicates to cache no JMS resources at all. + * @see #setCacheLevel + */ + public static final int CACHE_NONE = 0; + + /** + * Constant that indicates to cache a shared JMS Connection. + * @see #setCacheLevel + */ + public static final int CACHE_CONNECTION = 1; + + /** + * Constant that indicates to cache a shared JMS Connection + * and a JMS Session for each listener thread. + * @see #setCacheLevel + */ + public static final int CACHE_SESSION = 2; + + /** + * Constant that indicates to cache a shared JMS Connection + * and a JMS Session for each listener thread, as well as + * a JMS MessageConsumer for each listener thread. + * @see #setCacheLevel + */ + public static final int CACHE_CONSUMER = 3; + + /** + * Constant that indicates automatic choice of an appropriate + * caching level (depending on the transaction management strategy). + * @see #setCacheLevel + */ + public static final int CACHE_AUTO = 4; + + + private static final Constants constants = new Constants(DefaultMessageListenerContainer.class); + + + private TaskExecutor taskExecutor; + + private long recoveryInterval = DEFAULT_RECOVERY_INTERVAL; + + private int cacheLevel = CACHE_AUTO; + + private int concurrentConsumers = 1; + + private int maxConcurrentConsumers = 1; + + private int maxMessagesPerTask = Integer.MIN_VALUE; + + private int idleTaskExecutionLimit = 1; + + private final Set scheduledInvokers = new HashSet(); + + private int activeInvokerCount = 0; + + private Runnable stopCallback; + + private Object currentRecoveryMarker = new Object(); + + private final Object recoveryMonitor = new Object(); + + + /** + * Set the Spring TaskExecutor to use for running the listener threads. + *

Default is a {@link org.springframework.core.task.SimpleAsyncTaskExecutor}, + * starting up a number of new threads, according to the specified number + * of concurrent consumers. + *

Specify an alternative TaskExecutor for integration with an existing + * thread pool. Note that this really only adds value if the threads are + * managed in a specific fashion, for example within a J2EE environment. + * A plain thread pool does not add much value, as this listener container + * will occupy a number of threads for its entire lifetime. + * @see #setConcurrentConsumers + * @see org.springframework.core.task.SimpleAsyncTaskExecutor + * @see org.springframework.scheduling.commonj.WorkManagerTaskExecutor + */ + public void setTaskExecutor(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + /** + * Specify the interval between recovery attempts, in milliseconds. + * The default is 5000 ms, that is, 5 seconds. + * @see #handleListenerSetupFailure + */ + public void setRecoveryInterval(long recoveryInterval) { + this.recoveryInterval = recoveryInterval; + } + + /** + * Specify the level of caching that this listener container is allowed to apply, + * in the form of the name of the corresponding constant: e.g. "CACHE_CONNECTION". + * @see #setCacheLevel + */ + public void setCacheLevelName(String constantName) throws IllegalArgumentException { + if (constantName == null || !constantName.startsWith("CACHE_")) { + throw new IllegalArgumentException("Only cache constants allowed"); + } + setCacheLevel(constants.asNumber(constantName).intValue()); + } + + /** + * Specify the level of caching that this listener container is allowed to apply. + *

Default is CACHE_NONE if an external transaction manager has been specified + * (to reobtain all resources freshly within the scope of the external transaction), + * and CACHE_CONSUMER else (operating with local JMS resources). + *

Some J2EE servers only register their JMS resources with an ongoing XA + * transaction in case of a freshly obtained JMS Connection and Session, + * which is why this listener container does by default not cache any of those. + * However, if you want to optimize for a specific server, consider switching + * this setting to at least CACHE_CONNECTION or CACHE_SESSION even in + * conjunction with an external transaction manager. + *

Currently known servers that absolutely require CACHE_NONE for XA + * transaction processing: JBoss 4. For any others, consider raising the + * cache level. + * @see #CACHE_NONE + * @see #CACHE_CONNECTION + * @see #CACHE_SESSION + * @see #CACHE_CONSUMER + * @see #setCacheLevelName + * @see #setTransactionManager + */ + public void setCacheLevel(int cacheLevel) { + this.cacheLevel = cacheLevel; + } + + /** + * Return the level of caching that this listener container is allowed to apply. + */ + public int getCacheLevel() { + return this.cacheLevel; + } + + + /** + * Specify the number of concurrent consumers to create. Default is 1. + *

Specifying a higher value for this setting will increase the standard + * level of scheduled concurrent consumers at runtime: This is effectively + * the minimum number of concurrent consumers which will be scheduled + * at any given time. This is a static setting; for dynamic scaling, + * consider specifying the "maxConcurrentConsumers" setting instead. + *

Raising the number of concurrent consumers is recommendable in order + * to scale the consumption of messages coming in from a queue. However, + * note that any ordering guarantees are lost once multiple consumers are + * registered. In general, stick with 1 consumer for low-volume queues. + *

Do not raise the number of concurrent consumers for a topic. + * This would lead to concurrent consumption of the same message, + * which is hardly ever desirable. + *

This setting can be modified at runtime, for example through JMX. + * @see #setMaxConcurrentConsumers + */ + public void setConcurrentConsumers(int concurrentConsumers) { + Assert.isTrue(concurrentConsumers > 0, "'concurrentConsumers' value must be at least 1 (one)"); + synchronized (this.lifecycleMonitor) { + this.concurrentConsumers = concurrentConsumers; + if (this.maxConcurrentConsumers < concurrentConsumers) { + this.maxConcurrentConsumers = concurrentConsumers; + } + } + } + + /** + * Return the "concurrentConsumer" setting. + *

This returns the currently configured "concurrentConsumers" value; + * the number of currently scheduled/active consumers might differ. + * @see #getScheduledConsumerCount() + * @see #getActiveConsumerCount() + */ + public final int getConcurrentConsumers() { + synchronized (this.lifecycleMonitor) { + return this.concurrentConsumers; + } + } + + /** + * Specify the maximum number of concurrent consumers to create. Default is 1. + *

If this setting is higher than "concurrentConsumers", the listener container + * will dynamically schedule new consumers at runtime, provided that enough + * incoming messages are encountered. Once the load goes down again, the number of + * consumers will be reduced to the standard level ("concurrentConsumers") again. + *

Raising the number of concurrent consumers is recommendable in order + * to scale the consumption of messages coming in from a queue. However, + * note that any ordering guarantees are lost once multiple consumers are + * registered. In general, stick with 1 consumer for low-volume queues. + *

Do not raise the number of concurrent consumers for a topic. + * This would lead to concurrent consumption of the same message, + * which is hardly ever desirable. + *

This setting can be modified at runtime, for example through JMX. + * @see #setConcurrentConsumers + */ + public void setMaxConcurrentConsumers(int maxConcurrentConsumers) { + Assert.isTrue(maxConcurrentConsumers > 0, "'maxConcurrentConsumers' value must be at least 1 (one)"); + synchronized (this.lifecycleMonitor) { + this.maxConcurrentConsumers = + (maxConcurrentConsumers > this.concurrentConsumers ? maxConcurrentConsumers : this.concurrentConsumers); + } + } + + /** + * Return the "maxConcurrentConsumer" setting. + *

This returns the currently configured "maxConcurrentConsumers" value; + * the number of currently scheduled/active consumers might differ. + * @see #getScheduledConsumerCount() + * @see #getActiveConsumerCount() + */ + public final int getMaxConcurrentConsumers() { + synchronized (this.lifecycleMonitor) { + return this.maxConcurrentConsumers; + } + } + + /** + * Specify the maximum number of messages to process in one task. + * More concretely, this limits the number of message reception attempts + * per task, which includes receive iterations that did not actually + * pick up a message until they hit their timeout (see the + * {@link #setReceiveTimeout "receiveTimeout"} property). + *

Default is unlimited (-1) in case of a standard TaskExecutor, + * reusing the original invoker threads until shutdown (at the + * expense of limited dynamic scheduling). + *

In case of a SchedulingTaskExecutor indicating a preference for + * short-lived tasks, the default is 10 instead. Specify a number + * of 10 to 100 messages to balance between rather long-lived and + * rather short-lived tasks here. + *

Long-lived tasks avoid frequent thread context switches through + * sticking with the same thread all the way through, while short-lived + * tasks allow thread pools to control the scheduling. Hence, thread + * pools will usually prefer short-lived tasks. + *

This setting can be modified at runtime, for example through JMX. + * @see #setTaskExecutor + * @see #setReceiveTimeout + * @see org.springframework.scheduling.SchedulingTaskExecutor#prefersShortLivedTasks() + */ + public void setMaxMessagesPerTask(int maxMessagesPerTask) { + Assert.isTrue(maxMessagesPerTask != 0, "'maxMessagesPerTask' must not be 0"); + synchronized (this.lifecycleMonitor) { + this.maxMessagesPerTask = maxMessagesPerTask; + } + } + + /** + * Return the maximum number of messages to process in one task. + */ + public int getMaxMessagesPerTask() { + synchronized (this.lifecycleMonitor) { + return this.maxMessagesPerTask; + } + } + + /** + * Specify the limit for idle executions of a receive task, not having + * received any message within its execution. If this limit is reached, + * the task will shut down and leave receiving to other executing tasks. + *

Default is 1, closing idle resources early once a task didn't + * receive a message. This applies to dynamic scheduling only; see the + * {@link #setMaxConcurrentConsumers "maxConcurrentConsumers"} setting. + * The minimum number of consumers + * (see {@link #setConcurrentConsumers "concurrentConsumers"}) + * will be kept around until shutdown in any case. + *

Within each task execution, a number of message reception attempts + * (according to the "maxMessagesPerTask" setting) will each wait for an incoming + * message (according to the "receiveTimeout" setting). If all of those receive + * attempts in a given task return without a message, the task is considered + * idle with respect to received messages. Such a task may still be rescheduled; + * however, once it reached the specified "idleTaskExecutionLimit", it will + * shut down (in case of dynamic scaling). + *

Raise this limit if you encounter too frequent scaling up and down. + * With this limit being higher, an idle consumer will be kept around longer, + * avoiding the restart of a consumer once a new load of messages comes in. + * Alternatively, specify a higher "maxMessagesPerTask" and/or "receiveTimeout" value, + * which will also lead to idle consumers being kept around for a longer time + * (while also increasing the average execution time of each scheduled task). + *

This setting can be modified at runtime, for example through JMX. + * @see #setMaxMessagesPerTask + * @see #setReceiveTimeout + */ + public void setIdleTaskExecutionLimit(int idleTaskExecutionLimit) { + Assert.isTrue(idleTaskExecutionLimit > 0, "'idleTaskExecutionLimit' must be 1 or higher"); + synchronized (this.lifecycleMonitor) { + this.idleTaskExecutionLimit = idleTaskExecutionLimit; + } + } + + /** + * Return the limit for idle executions of a receive task. + */ + public int getIdleTaskExecutionLimit() { + synchronized (this.lifecycleMonitor) { + return this.idleTaskExecutionLimit; + } + } + + protected void validateConfiguration() { + super.validateConfiguration(); + synchronized (this.lifecycleMonitor) { + if (isSubscriptionDurable() && this.concurrentConsumers != 1) { + throw new IllegalArgumentException("Only 1 concurrent consumer supported for durable subscription"); + } + } + } + + + //------------------------------------------------------------------------- + // Implementation of AbstractMessageListenerContainer's template methods + //------------------------------------------------------------------------- + + public void initialize() { + // Adapt default cache level. + if (this.cacheLevel == CACHE_AUTO) { + this.cacheLevel = (getTransactionManager() != null ? CACHE_NONE : CACHE_CONSUMER); + } + + // Prepare taskExecutor and maxMessagesPerTask. + synchronized (this.lifecycleMonitor) { + if (this.taskExecutor == null) { + this.taskExecutor = createDefaultTaskExecutor(); + } + else if (this.taskExecutor instanceof SchedulingTaskExecutor && + ((SchedulingTaskExecutor) this.taskExecutor).prefersShortLivedTasks() && + this.maxMessagesPerTask == Integer.MIN_VALUE) { + // TaskExecutor indicated a preference for short-lived tasks. According to + // setMaxMessagesPerTask javadoc, we'll use 10 message per task in this case + // unless the user specified a custom value. + this.maxMessagesPerTask = 10; + } + } + + // Proceed with actual listener initialization. + super.initialize(); + } + + /** + * Creates the specified number of concurrent consumers, + * in the form of a JMS Session plus associated MessageConsumer + * running in a separate thread. + * @see #scheduleNewInvoker + * @see #setTaskExecutor + */ + protected void doInitialize() throws JMSException { + synchronized (this.lifecycleMonitor) { + for (int i = 0; i < this.concurrentConsumers; i++) { + scheduleNewInvoker(); + } + } + } + + /** + * Destroy the registered JMS Sessions and associated MessageConsumers. + */ + protected void doShutdown() throws JMSException { + logger.debug("Waiting for shutdown of message listener invokers"); + try { + synchronized (this.lifecycleMonitor) { + while (this.activeInvokerCount > 0) { + if (logger.isDebugEnabled()) { + logger.debug("Still waiting for shutdown of " + this.activeInvokerCount + + " message listener invokers"); + } + this.lifecycleMonitor.wait(); + } + } + } + catch (InterruptedException ex) { + // Re-interrupt current thread, to allow other threads to react. + Thread.currentThread().interrupt(); + } + } + + /** + * Overridden to reset the stop callback, if any. + */ + public void start() throws JmsException { + synchronized (this.lifecycleMonitor) { + this.stopCallback = null; + } + super.start(); + } + + /** + * Stop this listener container, invoking the specific callback + * once all listener processing has actually stopped. + *

Note: Further stop(runnable) calls (before processing + * has actually stopped) will override the specified callback. Only the + * latest specified callback will be invoked. + *

If a subsequent {@link #start()} call restarts the listener container + * before it has fully stopped, the callback will not get invoked at all. + * @param callback the callback to invoke once listener processing + * has fully stopped + * @throws JmsException if stopping failed + * @see #stop() + */ + public void stop(Runnable callback) throws JmsException { + synchronized (this.lifecycleMonitor) { + this.stopCallback = callback; + } + stop(); + } + + /** + * Return the number of currently scheduled consumers. + *

This number will always be inbetween "concurrentConsumers" and + * "maxConcurrentConsumers", but might be higher than "activeConsumerCount" + * (in case of some consumers being scheduled but not executed at the moment). + * @see #getConcurrentConsumers() + * @see #getMaxConcurrentConsumers() + * @see #getActiveConsumerCount() + */ + public final int getScheduledConsumerCount() { + synchronized (this.lifecycleMonitor) { + return this.scheduledInvokers.size(); + } + } + + /** + * Return the number of currently active consumers. + *

This number will always be inbetween "concurrentConsumers" and + * "maxConcurrentConsumers", but might be lower than "scheduledConsumerCount". + * (in case of some consumers being scheduled but not executed at the moment). + * @see #getConcurrentConsumers() + * @see #getMaxConcurrentConsumers() + * @see #getActiveConsumerCount() + */ + public final int getActiveConsumerCount() { + synchronized (this.lifecycleMonitor) { + return this.activeInvokerCount; + } + } + + + /** + * Create a default TaskExecutor. Called if no explicit TaskExecutor has been specified. + *

The default implementation builds a {@link org.springframework.core.task.SimpleAsyncTaskExecutor} + * with the specified bean name (or the class name, if no bean name specified) as thread name prefix. + * @see org.springframework.core.task.SimpleAsyncTaskExecutor#SimpleAsyncTaskExecutor(String) + */ + protected TaskExecutor createDefaultTaskExecutor() { + String beanName = getBeanName(); + String threadNamePrefix = (beanName != null ? beanName + "-" : DEFAULT_THREAD_NAME_PREFIX); + return new SimpleAsyncTaskExecutor(threadNamePrefix); + } + + /** + * Schedule a new invoker, increasing the total number of scheduled + * invokers for this listener container. + */ + private void scheduleNewInvoker() { + AsyncMessageListenerInvoker invoker = new AsyncMessageListenerInvoker(); + if (rescheduleTaskIfNecessary(invoker)) { + // This should always be true, since we're only calling this when active. + this.scheduledInvokers.add(invoker); + } + } + + /** + * Use a shared JMS Connection depending on the "cacheLevel" setting. + * @see #setCacheLevel + * @see #CACHE_CONNECTION + */ + protected final boolean sharedConnectionEnabled() { + return (getCacheLevel() >= CACHE_CONNECTION); + } + + /** + * Re-executes the given task via this listener container's TaskExecutor. + * @see #setTaskExecutor + */ + protected void doRescheduleTask(Object task) { + this.taskExecutor.execute((Runnable) task); + } + + /** + * Tries scheduling a new invoker, since we know messages are coming in... + * @see #scheduleNewInvokerIfAppropriate() + */ + protected void messageReceived(Object invoker, Session session) { + ((AsyncMessageListenerInvoker) invoker).setIdle(false); + scheduleNewInvokerIfAppropriate(); + } + + /** + * Marks the affected invoker as idle. + */ + protected void noMessageReceived(Object invoker, Session session) { + ((AsyncMessageListenerInvoker) invoker).setIdle(true); + } + + /** + * Schedule a new invoker, increasing the total number of scheduled + * invokers for this listener container, but only if the specified + * "maxConcurrentConsumers" limit has not been reached yet, and only + * if this listener container does not currently have idle invokers + * that are waiting for new messages already. + *

Called once a message has been received, to scale up while + * processing the message in the invoker that originally received it. + * @see #setTaskExecutor + * @see #getMaxConcurrentConsumers() + */ + protected void scheduleNewInvokerIfAppropriate() { + if (isRunning()) { + resumePausedTasks(); + synchronized (this.lifecycleMonitor) { + if (this.scheduledInvokers.size() < this.maxConcurrentConsumers && getIdleInvokerCount() == 0) { + scheduleNewInvoker(); + if (logger.isDebugEnabled()) { + logger.debug("Raised scheduled invoker count: " + this.scheduledInvokers.size()); + } + } + } + } + } + + /** + * Determine whether the current invoker should be rescheduled, + * given that it might not have received a message in a while. + * @param idleTaskExecutionCount the number of idle executions + * that this invoker task has already accumulated (in a row) + */ + private boolean shouldRescheduleInvoker(int idleTaskExecutionCount) { + boolean superfluous = + (idleTaskExecutionCount >= this.idleTaskExecutionLimit && getIdleInvokerCount() > 1); + return (this.scheduledInvokers.size() <= + (superfluous ? this.concurrentConsumers : this.maxConcurrentConsumers)); + } + + /** + * Determine whether this listener container currently has more + * than one idle instance among its scheduled invokers. + */ + private int getIdleInvokerCount() { + int count = 0; + for (Iterator it = this.scheduledInvokers.iterator(); it.hasNext();) { + AsyncMessageListenerInvoker invoker = (AsyncMessageListenerInvoker) it.next(); + if (invoker.isIdle()) { + count++; + } + } + return count; + } + + + /** + * Overridden to accept a failure in the initial setup - leaving it up to the + * asynchronous invokers to establish the shared Connection on first access. + * @see #refreshConnectionUntilSuccessful() + */ + protected void establishSharedConnection() { + try { + super.establishSharedConnection(); + } + catch (Exception ex) { + logger.debug("Could not establish shared JMS Connection - " + + "leaving it up to asynchronous invokers to establish a Connection as soon as possible", ex); + } + } + + /** + * This implementations proceeds even after an exception thrown from + * Connection.start(), relying on listeners to perform + * appropriate recovery. + */ + protected void startSharedConnection() { + try { + super.startSharedConnection(); + } + catch (Exception ex) { + logger.debug("Connection start failed - relying on listeners to perform recovery", ex); + } + } + + /** + * This implementations proceeds even after an exception thrown from + * Connection.stop(), relying on listeners to perform + * appropriate recovery after a restart. + */ + protected void stopSharedConnection() { + try { + super.stopSharedConnection(); + } + catch (Exception ex) { + logger.debug("Connection stop failed - relying on listeners to perform recovery after restart", ex); + } + } + + /** + * Handle the given exception that arose during setup of a listener. + * Called for every such exception in every concurrent listener. + *

The default implementation logs the exception at error level + * if not recovered yet, and at debug level if already recovered. + * Can be overridden in subclasses. + * @param ex the exception to handle + * @param alreadyRecovered whether a previously executing listener + * already recovered from the present listener setup failure + * (this usually indicates a follow-up failure than can be ignored + * other than for debug log purposes) + * @see #recoverAfterListenerSetupFailure() + */ + protected void handleListenerSetupFailure(Throwable ex, boolean alreadyRecovered) { + if (ex instanceof JMSException) { + invokeExceptionListener((JMSException) ex); + } + if (ex instanceof SharedConnectionNotInitializedException) { + if (!alreadyRecovered) { + logger.debug("JMS message listener invoker needs to establish shared Connection"); + } + } + else { + // Recovery during active operation.. + if (alreadyRecovered) { + logger.debug("Setup of JMS message listener invoker failed - already recovered by other invoker", ex); + } + else { + StringBuffer msg = new StringBuffer(); + msg.append("Setup of JMS message listener invoker failed for destination '"); + msg.append(getDestinationDescription()).append("' - trying to recover. Cause: "); + msg.append(ex instanceof JMSException ? JmsUtils.buildExceptionMessage((JMSException) ex) : ex.getMessage()); + if (logger.isDebugEnabled()) { + logger.info(msg, ex); + } + else { + logger.info(msg); + } + } + } + } + + /** + * Recover this listener container after a listener failed to set itself up, + * for example reestablishing the underlying Connection. + *

The default implementation delegates to DefaultMessageListenerContainer's + * recovery-capable {@link #refreshConnectionUntilSuccessful()} method, which will + * try to re-establish a Connection to the JMS provider both for the shared + * and the non-shared Connection case. + * @see #refreshConnectionUntilSuccessful() + * @see #refreshDestination() + */ + protected void recoverAfterListenerSetupFailure() { + refreshConnectionUntilSuccessful(); + refreshDestination(); + } + + /** + * Refresh the underlying Connection, not returning before an attempt has been + * successful. Called in case of a shared Connection as well as without shared + * Connection, so either needs to operate on the shared Connection or on a + * temporary Connection that just gets established for validation purposes. + *

The default implementation retries until it successfully established a + * Connection, for as long as this message listener container is active. + * Applies the specified recovery interval between retries. + * @see #setRecoveryInterval + */ + protected void refreshConnectionUntilSuccessful() { + while (isRunning()) { + try { + if (sharedConnectionEnabled()) { + refreshSharedConnection(); + } + else { + Connection con = createConnection(); + JmsUtils.closeConnection(con); + } + logger.info("Successfully refreshed JMS Connection"); + break; + } + catch (Exception ex) { + StringBuffer msg = new StringBuffer(); + msg.append("Could not refresh JMS Connection for destination '"); + msg.append(getDestinationDescription()).append("' - retrying in "); + msg.append(this.recoveryInterval).append(" ms. Cause: "); + msg.append(ex instanceof JMSException ? JmsUtils.buildExceptionMessage((JMSException) ex) : ex.getMessage()); + if (logger.isDebugEnabled()) { + logger.info(msg, ex); + } + else if (logger.isInfoEnabled()) { + logger.info(msg); + } + } + sleepInbetweenRecoveryAttempts(); + } + } + + /** + * Refresh the JMS destination that this listener container operates on. + *

Called after listener setup failure, assuming that a cached Destination + * object might have become invalid (a typical case on WebLogic JMS). + *

The default implementation removes the destination from a + * DestinationResolver's cache, in case of a CachingDestinationResolver. + * @see #setDestinationName + * @see org.springframework.jms.support.destination.CachingDestinationResolver + */ + protected void refreshDestination() { + String destName = getDestinationName(); + if (destName != null) { + DestinationResolver destResolver = getDestinationResolver(); + if (destResolver instanceof CachingDestinationResolver) { + ((CachingDestinationResolver) destResolver).removeFromCache(destName); + } + } + } + + /** + * Sleep according to the specified recovery interval. + * Called inbetween recovery attempts. + */ + protected void sleepInbetweenRecoveryAttempts() { + if (this.recoveryInterval > 0) { + try { + Thread.sleep(this.recoveryInterval); + } + catch (InterruptedException interEx) { + // Re-interrupt current thread, to allow other threads to react. + Thread.currentThread().interrupt(); + } + } + } + + + //------------------------------------------------------------------------- + // Inner classes used as internal adapters + //------------------------------------------------------------------------- + + /** + * Runnable that performs looped MessageConsumer.receive() calls. + */ + private class AsyncMessageListenerInvoker implements SchedulingAwareRunnable { + + private Session session; + + private MessageConsumer consumer; + + private Object lastRecoveryMarker; + + private boolean lastMessageSucceeded; + + private int idleTaskExecutionCount = 0; + + private volatile boolean idle = true; + + public void run() { + synchronized (lifecycleMonitor) { + activeInvokerCount++; + lifecycleMonitor.notifyAll(); + } + boolean messageReceived = false; + try { + if (maxMessagesPerTask < 0) { + messageReceived = executeOngoingLoop(); + } + else { + int messageCount = 0; + while (isRunning() && messageCount < maxMessagesPerTask) { + messageReceived = (invokeListener() || messageReceived); + messageCount++; + } + } + } + catch (Throwable ex) { + clearResources(); + if (!this.lastMessageSucceeded) { + // We failed more than once in a row - sleep for recovery interval + // even before first recovery attempt. + sleepInbetweenRecoveryAttempts(); + } + this.lastMessageSucceeded = false; + boolean alreadyRecovered = false; + synchronized (recoveryMonitor) { + if (this.lastRecoveryMarker == currentRecoveryMarker) { + handleListenerSetupFailure(ex, false); + recoverAfterListenerSetupFailure(); + currentRecoveryMarker = new Object(); + } + else { + alreadyRecovered = true; + } + } + if (alreadyRecovered) { + handleListenerSetupFailure(ex, true); + } + } + synchronized (lifecycleMonitor) { + decreaseActiveInvokerCount(); + lifecycleMonitor.notifyAll(); + } + if (!messageReceived) { + this.idleTaskExecutionCount++; + } + else { + this.idleTaskExecutionCount = 0; + } + synchronized (lifecycleMonitor) { + if (!shouldRescheduleInvoker(this.idleTaskExecutionCount) || !rescheduleTaskIfNecessary(this)) { + // We're shutting down completely. + scheduledInvokers.remove(this); + if (logger.isDebugEnabled()) { + logger.debug("Lowered scheduled invoker count: " + scheduledInvokers.size()); + } + lifecycleMonitor.notifyAll(); + clearResources(); + } + else if (isRunning()) { + int nonPausedConsumers = getScheduledConsumerCount() - getPausedTaskCount(); + if (nonPausedConsumers < 1) { + logger.error("All scheduled consumers have been paused, probably due to tasks having been rejected. " + + "Check your thread pool configuration! Manual recovery necessary through a start() call."); + } + else if (nonPausedConsumers < getConcurrentConsumers()) { + logger.warn("Number of scheduled consumers has dropped below concurrentConsumers limit, probably " + + "due to tasks having been rejected. Check your thread pool configuration! Automatic recovery " + + "to be triggered by remaining consumers."); + } + } + } + } + + private boolean executeOngoingLoop() throws JMSException { + boolean messageReceived = false; + boolean active = true; + while (active) { + synchronized (lifecycleMonitor) { + boolean interrupted = false; + boolean wasWaiting = false; + while ((active = isActive()) && !isRunning()) { + if (interrupted) { + throw new IllegalStateException("Thread was interrupted while waiting for " + + "a restart of the listener container, but container is still stopped"); + } + if (!wasWaiting) { + decreaseActiveInvokerCount(); + } + wasWaiting = true; + try { + lifecycleMonitor.wait(); + } + catch (InterruptedException ex) { + // Re-interrupt current thread, to allow other threads to react. + Thread.currentThread().interrupt(); + interrupted = true; + } + } + if (wasWaiting) { + activeInvokerCount++; + } + } + if (active) { + messageReceived = (invokeListener() || messageReceived); + } + } + return messageReceived; + } + + private boolean invokeListener() throws JMSException { + initResourcesIfNecessary(); + boolean messageReceived = receiveAndExecute(this, this.session, this.consumer); + this.lastMessageSucceeded = true; + return messageReceived; + } + + private void decreaseActiveInvokerCount() { + activeInvokerCount--; + if (stopCallback != null && activeInvokerCount == 0) { + stopCallback.run(); + stopCallback = null; + } + } + + private void initResourcesIfNecessary() throws JMSException { + if (getCacheLevel() <= CACHE_CONNECTION) { + updateRecoveryMarker(); + } + else { + if (this.session == null && getCacheLevel() >= CACHE_SESSION) { + updateRecoveryMarker(); + this.session = createSession(getSharedConnection()); + } + if (this.consumer == null && getCacheLevel() >= CACHE_CONSUMER) { + this.consumer = createListenerConsumer(this.session); + } + } + } + + private void updateRecoveryMarker() { + synchronized (recoveryMonitor) { + this.lastRecoveryMarker = currentRecoveryMarker; + } + } + + private void clearResources() { + if (sharedConnectionEnabled()) { + synchronized (sharedConnectionMonitor) { + JmsUtils.closeMessageConsumer(this.consumer); + JmsUtils.closeSession(this.session); + } + } + else { + JmsUtils.closeMessageConsumer(this.consumer); + JmsUtils.closeSession(this.session); + } + this.consumer = null; + this.session = null; + } + + public boolean isLongLived() { + return (maxMessagesPerTask < 0); + } + + public void setIdle(boolean idle) { + this.idle = idle; + } + + public boolean isIdle() { + return this.idle; + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer102.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer102.java new file mode 100644 index 00000000000..63c71f30cf1 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer102.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2007 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 javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MessageConsumer; +import javax.jms.Queue; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; +import javax.jms.TopicSession; + +import org.springframework.jms.connection.JmsResourceHolder; + +/** + * A subclass of {@link DefaultMessageListenerContainer} for the JMS 1.0.2 specification, + * not relying on JMS 1.1 methods like SimpleMessageListenerContainer itself. + * + *

This class can be used for JMS 1.0.2 providers, offering the same facility as + * DefaultMessageListenerContainer does for JMS 1.1 providers. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class DefaultMessageListenerContainer102 extends DefaultMessageListenerContainer { + + /** + * This implementation overrides the superclass method to accept either + * a QueueConnection or a TopicConnection, depending on the domain. + */ + protected Connection getConnection(JmsResourceHolder holder) { + return holder.getConnection(isPubSubDomain() ? (Class) TopicConnection.class : QueueConnection.class); + } + + /** + * This implementation overrides the superclass method to accept either + * a QueueSession or a TopicSession, depending on the domain. + */ + protected Session getSession(JmsResourceHolder holder) { + return holder.getSession(isPubSubDomain() ? (Class) TopicSession.class : QueueSession.class); + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Connection createConnection() throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnectionFactory) getConnectionFactory()).createTopicConnection(); + } + else { + return ((QueueConnectionFactory) getConnectionFactory()).createQueueConnection(); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Session createSession(Connection con) throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnection) con).createTopicSession(isSessionTransacted(), getSessionAcknowledgeMode()); + } + else { + return ((QueueConnection) con).createQueueSession(isSessionTransacted(), getSessionAcknowledgeMode()); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected MessageConsumer createConsumer(Session session, Destination destination) throws JMSException { + if (isPubSubDomain()) { + if (isSubscriptionDurable()) { + return ((TopicSession) session).createDurableSubscriber( + (Topic) destination, getDurableSubscriptionName(), getMessageSelector(), isPubSubNoLocal()); + } + else { + return ((TopicSession) session).createSubscriber( + (Topic) destination, getMessageSelector(), isPubSubNoLocal()); + } + } + else { + return ((QueueSession) session).createReceiver((Queue) destination, getMessageSelector()); + } + } + + /** + * This implementation overrides the superclass method to avoid using + * JMS 1.1's Session getAcknowledgeMode() method. + * The best we can do here is to check the setting on the listener container. + */ + protected boolean isClientAcknowledge(Session session) throws JMSException { + return (getSessionAcknowledgeMode() == Session.CLIENT_ACKNOWLEDGE); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/LocallyExposedJmsResourceHolder.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/LocallyExposedJmsResourceHolder.java new file mode 100644 index 00000000000..118f179a8eb --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/LocallyExposedJmsResourceHolder.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2008 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 javax.jms.Session; + +import org.springframework.jms.connection.JmsResourceHolder; + +/** + * JmsResourceHolder marker subclass that indicates local exposure, + * i.e. that does not indicate an externally managed transaction. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +class LocallyExposedJmsResourceHolder extends JmsResourceHolder { + + public LocallyExposedJmsResourceHolder(Session session) { + super(session); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/SessionAwareMessageListener.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/SessionAwareMessageListener.java new file mode 100644 index 00000000000..1b9e8f3a806 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/SessionAwareMessageListener.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2008 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 javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Session; + +/** + * Variant of the standard JMS {@link javax.jms.MessageListener} interface, + * offering not only the received Message but also the underlying + * JMS Session object. The latter can be used to send reply messages, + * without the need to access an external Connection/Session, + * i.e. without the need to access the underlying ConnectionFactory. + * + *

Supported by Spring's {@link DefaultMessageListenerContainer} + * and {@link SimpleMessageListenerContainer}, + * as direct alternative to the standard JMS MessageListener interface. + * Typically not supported by JCA-based listener containers: + * For maximum compatibility, implement a standard JMS MessageListener instead. + * + * @author Juergen Hoeller + * @since 2.0 + * @see AbstractMessageListenerContainer#setMessageListener + * @see DefaultMessageListenerContainer + * @see SimpleMessageListenerContainer + * @see org.springframework.jms.listener.endpoint.JmsMessageEndpointManager + * @see javax.jms.MessageListener + */ +public interface SessionAwareMessageListener { + + /** + * Callback for processing a received JMS message. + *

Implementors are supposed to process the given Message, + * typically sending reply messages through the given Session. + * @param message the received JMS message (never null) + * @param session the underlying JMS Session (never null) + * @throws JMSException if thrown by JMS methods + */ + void onMessage(Message message, Session session) throws JMSException; + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java new file mode 100644 index 00000000000..472e17cac92 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java @@ -0,0 +1,346 @@ +/* + * Copyright 2002-2008 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 java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.ExceptionListener; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageListener; +import javax.jms.Session; +import javax.jms.Topic; + +import org.springframework.core.task.TaskExecutor; +import org.springframework.jms.support.JmsUtils; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * Message listener container that uses the plain JMS client API's + * MessageConsumer.setMessageListener() method to + * create concurrent MessageConsumers for the specified listeners. + * + *

NOTE: This class requires a JMS 1.1+ provider, because it builds on + * the domain-independent API. Use the {@link SimpleMessageListenerContainer102} + * subclass for a JMS 1.0.2 provider, e.g. when running on a J2EE 1.3 server. + * + *

This is the simplest form of a message listener container. + * It creates a fixed number of JMS Sessions to invoke the listener, + * not allowing for dynamic adaptation to runtime demands. Its main + * advantage is its low level of complexity and the minimum requirements + * on the JMS provider: Not even the ServerSessionPool facility is required. + * + *

See the {@link AbstractMessageListenerContainer} javadoc for details + * on acknowledge modes and transaction options. + * + *

For a different style of MessageListener handling, through looped + * MessageConsumer.receive() calls that also allow for + * transactional reception of messages (registering them with XA transactions), + * see {@link DefaultMessageListenerContainer}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see javax.jms.MessageConsumer#setMessageListener + * @see DefaultMessageListenerContainer + * @see org.springframework.jms.listener.endpoint.JmsMessageEndpointManager + */ +public class SimpleMessageListenerContainer extends AbstractMessageListenerContainer implements ExceptionListener { + + private boolean pubSubNoLocal = false; + + private int concurrentConsumers = 1; + + private TaskExecutor taskExecutor; + + private Set sessions; + + private Set consumers; + + private final Object consumersMonitor = new Object(); + + + /** + * Set whether to inhibit the delivery of messages published by its own connection. + * Default is "false". + * @see javax.jms.TopicSession#createSubscriber(javax.jms.Topic, String, boolean) + */ + public void setPubSubNoLocal(boolean pubSubNoLocal) { + this.pubSubNoLocal = pubSubNoLocal; + } + + /** + * Return whether to inhibit the delivery of messages published by its own connection. + */ + protected boolean isPubSubNoLocal() { + return this.pubSubNoLocal; + } + + /** + * Specify the number of concurrent consumers to create. Default is 1. + *

Raising the number of concurrent consumers is recommendable in order + * to scale the consumption of messages coming in from a queue. However, + * note that any ordering guarantees are lost once multiple consumers are + * registered. In general, stick with 1 consumer for low-volume queues. + *

Do not raise the number of concurrent consumers for a topic. + * This would lead to concurrent consumption of the same message, + * which is hardly ever desirable. + */ + public void setConcurrentConsumers(int concurrentConsumers) { + Assert.isTrue(concurrentConsumers > 0, "'concurrentConsumers' value must be at least 1 (one)"); + this.concurrentConsumers = concurrentConsumers; + } + + /** + * Set the Spring TaskExecutor to use for executing the listener once + * a message has been received by the provider. + *

Default is none, that is, to run in the JMS provider's own receive thread, + * blocking the provider's receive endpoint while executing the listener. + *

Specify a TaskExecutor for executing the listener in a different thread, + * rather than blocking the JMS provider, usually integrating with an existing + * thread pool. This allows to keep the number of concurrent consumers low (1) + * while still processing messages concurrently (decoupled from receiving!). + *

NOTE: Specifying a TaskExecutor for listener execution affects + * acknowledgement semantics. Messages will then always get acknowledged + * before listener execution, with the underlying Session immediately reused + * for receiving the next message. Using this in combination with a transacted + * session or with client acknowledgement will lead to unspecified results! + *

NOTE: Concurrent listener execution via a TaskExecutor will lead + * to concurrent processing of messages that have been received by the same + * underlying Session. As a consequence, it is not recommended to use + * this setting with a {@link SessionAwareMessageListener}, at least not + * if the latter performs actual work on the given Session. A standard + * {@link javax.jms.MessageListener} will work fine, in general. + * @see #setConcurrentConsumers + * @see org.springframework.core.task.SimpleAsyncTaskExecutor + * @see org.springframework.scheduling.commonj.WorkManagerTaskExecutor + */ + public void setTaskExecutor(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + protected void validateConfiguration() { + super.validateConfiguration(); + if (isSubscriptionDurable() && this.concurrentConsumers != 1) { + throw new IllegalArgumentException("Only 1 concurrent consumer supported for durable subscription"); + } + } + + + //------------------------------------------------------------------------- + // Implementation of AbstractMessageListenerContainer's template methods + //------------------------------------------------------------------------- + + /** + * Always use a shared JMS Connection. + */ + protected final boolean sharedConnectionEnabled() { + return true; + } + + /** + * Creates the specified number of concurrent consumers, + * in the form of a JMS Session plus associated MessageConsumer. + * @see #createListenerConsumer + */ + protected void doInitialize() throws JMSException { + establishSharedConnection(); + initializeConsumers(); + } + + /** + * Re-initializes this container's JMS message consumers, + * if not initialized already. + */ + protected void doStart() throws JMSException { + super.doStart(); + initializeConsumers(); + } + + /** + * Registers this listener container as JMS ExceptionListener on the shared connection. + */ + protected void prepareSharedConnection(Connection connection) throws JMSException { + super.prepareSharedConnection(connection); + connection.setExceptionListener(this); + } + + /** + * JMS ExceptionListener implementation, invoked by the JMS provider in + * case of connection failures. Re-initializes this listener container's + * shared connection and its sessions and consumers. + * @param ex the reported connection exception + */ + public void onException(JMSException ex) { + // First invoke the user-specific ExceptionListener, if any. + invokeExceptionListener(ex); + + // Now try to recover the shared Connection and all consumers... + if (logger.isInfoEnabled()) { + logger.info("Trying to recover from JMS Connection exception: " + ex); + } + try { + synchronized (this.consumersMonitor) { + this.sessions = null; + this.consumers = null; + } + refreshSharedConnection(); + initializeConsumers(); + logger.info("Successfully refreshed JMS Connection"); + } + catch (JMSException recoverEx) { + logger.debug("Failed to recover JMS Connection", recoverEx); + logger.error("Encountered non-recoverable JMSException", ex); + } + } + + /** + * Initialize the JMS Sessions and MessageConsumers for this container. + * @throws JMSException in case of setup failure + */ + protected void initializeConsumers() throws JMSException { + // Register Sessions and MessageConsumers. + synchronized (this.consumersMonitor) { + if (this.consumers == null) { + this.sessions = new HashSet(this.concurrentConsumers); + this.consumers = new HashSet(this.concurrentConsumers); + Connection con = getSharedConnection(); + for (int i = 0; i < this.concurrentConsumers; i++) { + Session session = createSession(con); + MessageConsumer consumer = createListenerConsumer(session); + this.sessions.add(session); + this.consumers.add(consumer); + } + } + } + } + + /** + * Create a MessageConsumer for the given JMS Session, + * registering a MessageListener for the specified listener. + * @param session the JMS Session to work on + * @return the MessageConsumer + * @throws JMSException if thrown by JMS methods + * @see #executeListener + */ + protected MessageConsumer createListenerConsumer(final Session session) throws JMSException { + Destination destination = getDestination(); + if (destination == null) { + destination = resolveDestinationName(session, getDestinationName()); + } + MessageConsumer consumer = createConsumer(session, destination); + + if (this.taskExecutor != null) { + consumer.setMessageListener(new MessageListener() { + public void onMessage(final Message message) { + taskExecutor.execute(new Runnable() { + public void run() { + processMessage(message, session); + } + }); + } + }); + } + else { + consumer.setMessageListener(new MessageListener() { + public void onMessage(Message message) { + processMessage(message, session); + } + }); + } + + return consumer; + } + + /** + * Process a message received from the provider. + *

Executes the listener, exposing the current JMS Session as + * thread-bound resource (if "exposeListenerSession" is "true"). + * @param message the received JMS Message + * @param session the JMS Session to operate on + * @see #executeListener + * @see #setExposeListenerSession + */ + protected void processMessage(Message message, Session session) { + boolean exposeResource = isExposeListenerSession(); + if (exposeResource) { + TransactionSynchronizationManager.bindResource( + getConnectionFactory(), new LocallyExposedJmsResourceHolder(session)); + } + try { + executeListener(session, message); + } + finally { + if (exposeResource) { + TransactionSynchronizationManager.unbindResource(getConnectionFactory()); + } + } + } + + /** + * Destroy the registered JMS Sessions and associated MessageConsumers. + */ + protected void doShutdown() throws JMSException { + logger.debug("Closing JMS MessageConsumers"); + for (Iterator it = this.consumers.iterator(); it.hasNext();) { + MessageConsumer consumer = (MessageConsumer) it.next(); + JmsUtils.closeMessageConsumer(consumer); + } + logger.debug("Closing JMS Sessions"); + for (Iterator it = this.sessions.iterator(); it.hasNext();) { + Session session = (Session) it.next(); + JmsUtils.closeSession(session); + } + } + + + //------------------------------------------------------------------------- + // JMS 1.1 factory methods, potentially overridden for JMS 1.0.2 + //------------------------------------------------------------------------- + + /** + * Create a JMS MessageConsumer for the given Session and Destination. + *

This implementation uses JMS 1.1 API. + * @param session the JMS Session to create a MessageConsumer for + * @param destination the JMS Destination to create a MessageConsumer for + * @return the new JMS MessageConsumer + * @throws JMSException if thrown by JMS API methods + */ + protected MessageConsumer createConsumer(Session session, Destination destination) throws JMSException { + // Only pass in the NoLocal flag in case of a Topic: + // Some JMS providers, such as WebSphere MQ 6.0, throw IllegalStateException + // in case of the NoLocal flag being specified for a Queue. + if (isPubSubDomain()) { + if (isSubscriptionDurable() && destination instanceof Topic) { + return session.createDurableSubscriber( + (Topic) destination, getDurableSubscriptionName(), getMessageSelector(), isPubSubNoLocal()); + } + else { + return session.createConsumer(destination, getMessageSelector(), isPubSubNoLocal()); + } + } + else { + return session.createConsumer(destination, getMessageSelector()); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer102.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer102.java new file mode 100644 index 00000000000..beecd5f20ae --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer102.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2007 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 javax.jms.Connection; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.MessageConsumer; +import javax.jms.Queue; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; +import javax.jms.TopicSession; + +/** + * A subclass of {@link SimpleMessageListenerContainer} for the JMS 1.0.2 specification, + * not relying on JMS 1.1 methods like SimpleMessageListenerContainer itself. + * + *

This class can be used for JMS 1.0.2 providers, offering the same facility as + * SimpleMessageListenerContainer does for JMS 1.1 providers. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class SimpleMessageListenerContainer102 extends SimpleMessageListenerContainer { + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Connection createConnection() throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnectionFactory) getConnectionFactory()).createTopicConnection(); + } + else { + return ((QueueConnectionFactory) getConnectionFactory()).createQueueConnection(); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Session createSession(Connection con) throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnection) con).createTopicSession(isSessionTransacted(), getSessionAcknowledgeMode()); + } + else { + return ((QueueConnection) con).createQueueSession(isSessionTransacted(), getSessionAcknowledgeMode()); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected MessageConsumer createConsumer(Session session, Destination destination) throws JMSException { + if (isPubSubDomain()) { + if (isSubscriptionDurable()) { + return ((TopicSession) session).createDurableSubscriber( + (Topic) destination, getDurableSubscriptionName(), getMessageSelector(), isPubSubNoLocal()); + } + else { + return ((TopicSession) session).createSubscriber( + (Topic) destination, getMessageSelector(), isPubSubNoLocal()); + } + } + else { + return ((QueueSession) session).createReceiver((Queue) destination, getMessageSelector()); + } + } + + /** + * This implementation overrides the superclass method to avoid using + * JMS 1.1's Session getAcknowledgeMode() method. + * The best we can do here is to check the setting on the listener container. + */ + protected boolean isClientAcknowledge(Session session) throws JMSException { + return (getSessionAcknowledgeMode() == Session.CLIENT_ACKNOWLEDGE); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/SubscriptionNameProvider.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/SubscriptionNameProvider.java new file mode 100644 index 00000000000..c8c52395d6a --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/SubscriptionNameProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2008 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; + +/** + * Interface to be implemented by message listener objects that suggest a specific + * name for a durable subscription that they might be registered with. Otherwise + * the listener class name will be used as a default subscription name. + * + *

Applies to {@link javax.jms.MessageListener} objects as well as to + * {@link SessionAwareMessageListener} objects and plain listener methods + * (as supported by {@link org.springframework.jms.listener.adapter.MessageListenerAdapter}. + * + * @author Juergen Hoeller + * @since 2.5.6 + */ +public interface SubscriptionNameProvider { + + /** + * Determine the subscription name for this message listener object. + */ + String getSubscriptionName(); + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/ListenerExecutionFailedException.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/ListenerExecutionFailedException.java new file mode 100644 index 00000000000..4cdfdb8f209 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/ListenerExecutionFailedException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2006 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 org.springframework.jms.JmsException; + +/** + * Exception to be thrown when the execution of a listener method failed. + * + * @author Juergen Hoeller + * @since 2.0 + * @see MessageListenerAdapter + */ +public class ListenerExecutionFailedException extends JmsException { + + /** + * Constructor for ListenerExecutionFailedException. + * @param msg the detail message + * @param cause the exception thrown by the listener method + */ + public ListenerExecutionFailedException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java new file mode 100644 index 00000000000..f666d9ebdc3 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java @@ -0,0 +1,656 @@ +/* + * Copyright 2002-2008 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.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; + +/** + * Message listener adapter that delegates the handling of messages to target + * listener methods via reflection, with flexible message type conversion. + * Allows listener methods to operate on message content types, completely + * independent from the JMS API. + * + *

NOTE: This class requires a JMS 1.1+ provider, because it builds + * on the domain-independent API. Use the {@link MessageListenerAdapter102 + * MessageListenerAdapter102} subclass for JMS 1.0.2 providers. + * + *

By default, the content of incoming JMS messages gets extracted before + * being passed into the target listener method, to let the target method + * operate on message content types such as String or byte array instead of + * the raw {@link Message}. Message type conversion is delegated to a Spring + * JMS {@link MessageConverter}. By default, a {@link SimpleMessageConverter} + * {@link org.springframework.jms.support.converter.SimpleMessageConverter102 (102)} + * will be used. (If you do not want such automatic message conversion taking + * place, then be sure to set the {@link #setMessageConverter MessageConverter} + * to null.) + * + *

If a target listener method returns a non-null object (typically of a + * message content type such as String or byte array), it will get + * wrapped in a JMS Message and sent to the response destination + * (either the JMS "reply-to" destination or a + * {@link #setDefaultResponseDestination(javax.jms.Destination) specified default + * destination}). + * + *

Note: The sending of response messages is only available when + * using the {@link SessionAwareMessageListener} entry point (typically through a + * Spring message listener container). Usage as standard JMS {@link MessageListener} + * does not support the generation of response messages. + * + *

Find below some examples of method signatures compliant with this + * adapter class. This first example handles all Message types + * and gets passed the contents of each Message type as an + * argument. No Message will be sent back as all of these + * methods return void. + * + *

public interface MessageContentsDelegate {
+ *    void handleMessage(String text);
+ *    void handleMessage(Map map);
+ *    void handleMessage(byte[] bytes);
+ *    void handleMessage(Serializable obj);
+ * }
+ * + * This next example handles all Message types and gets + * passed the actual (raw) Message as an argument. Again, no + * Message will be sent back as all of these methods return + * void. + * + *
public interface RawMessageDelegate {
+ *    void handleMessage(TextMessage message);
+ *    void handleMessage(MapMessage message);
+ *    void handleMessage(BytesMessage message);
+ *    void handleMessage(ObjectMessage message);
+ * }
+ * + * This next example illustrates a Message delegate + * that just consumes the String contents of + * {@link javax.jms.TextMessage TextMessages}. Notice also how the + * name of the Message handling method is different from the + * {@link #ORIGINAL_DEFAULT_LISTENER_METHOD original} (this will have to + * be configured in the attandant bean definition). Again, no Message + * will be sent back as the method returns void. + * + *
public interface TextMessageContentDelegate {
+ *    void onMessage(String text);
+ * }
+ * + * This final example illustrates a Message delegate + * that just consumes the String contents of + * {@link javax.jms.TextMessage TextMessages}. Notice how the return type + * of this method is String: This will result in the configured + * {@link MessageListenerAdapter} sending a {@link javax.jms.TextMessage} in response. + * + *
public interface ResponsiveTextMessageContentDelegate {
+ *    String handleMessage(String text);
+ * }
+ * + * For further examples and discussion please do refer to the Spring + * reference documentation which describes this class (and it's attendant + * XML configuration) in detail. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setDelegate + * @see #setDefaultListenerMethod + * @see #setDefaultResponseDestination + * @see #setMessageConverter + * @see org.springframework.jms.support.converter.SimpleMessageConverter + * @see org.springframework.jms.listener.SessionAwareMessageListener + * @see org.springframework.jms.listener.AbstractMessageListenerContainer#setMessageListener + */ +public class MessageListenerAdapter implements MessageListener, SessionAwareMessageListener, SubscriptionNameProvider { + + /** + * Out-of-the-box value for the default listener method: "handleMessage". + */ + 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. + */ + public MessageListenerAdapter() { + initDefaultStrategies(); + this.delegate = this; + } + + /** + * Create a new {@link MessageListenerAdapter} for the given delegate. + * @param delegate the delegate object + */ + public MessageListenerAdapter(Object delegate) { + initDefaultStrategies(); + setDelegate(delegate); + } + + + /** + * Set a target object to delegate message listening to. + * Specified listener methods have to be present on this target object. + *

If no explicit delegate object has been specified, listener + * methods are expected to present on this adapter instance, that is, + * on a custom subclass of this adapter, defining listener methods. + */ + public void setDelegate(Object delegate) { + Assert.notNull(delegate, "Delegate must not be null"); + this.delegate = delegate; + } + + /** + * Return the target object to delegate message listening to. + */ + protected Object getDelegate() { + return this.delegate; + } + + /** + * Specify the name of the default listener method to delegate to, + * for the case where no specific listener method has been determined. + * Out-of-the-box value is {@link #ORIGINAL_DEFAULT_LISTENER_METHOD "handleMessage"}. + * @see #getListenerMethodName + */ + public void setDefaultListenerMethod(String defaultListenerMethod) { + this.defaultListenerMethod = defaultListenerMethod; + } + + /** + * Return the name of the default listener method to delegate to. + */ + protected String getDefaultListenerMethod() { + 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) + */ + 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 + * conversion of the message argument. If the target method returns a + * non-null object, wrap in a JMS message and send it back. + * @param message the incoming JMS message + * @param session the JMS session to operate on + * @throws JMSException if thrown by JMS API methods + */ + public void onMessage(Message message, Session session) throws JMSException { + // Check whether the delegate is a MessageListener impl itself. + // In that case, the adapter will simply act as a pass-through. + Object delegate = getDelegate(); + if (delegate != this) { + if (delegate instanceof SessionAwareMessageListener) { + if (session != null) { + ((SessionAwareMessageListener) delegate).onMessage(message, session); + return; + } + else if (!(delegate instanceof MessageListener)) { + throw new javax.jms.IllegalStateException("MessageListenerAdapter cannot handle a " + + "SessionAwareMessageListener delegate if it hasn't been invoked with a Session itself"); + } + } + if (delegate instanceof MessageListener) { + ((MessageListener) delegate).onMessage(message); + return; + } + } + + // Regular case: find a handler method reflectively. + Object convertedMessage = extractMessage(message); + String methodName = getListenerMethodName(message, convertedMessage); + if (methodName == null) { + throw new javax.jms.IllegalStateException("No default listener method specified: " + + "Either specify a non-null value for the 'defaultListenerMethod' property or " + + "override the 'getListenerMethodName' method."); + } + + // Invoke the handler method with appropriate arguments. + Object[] listenerArguments = buildListenerArguments(convertedMessage); + Object result = invokeListenerMethod(methodName, listenerArguments); + if (result != null) { + handleResult(result, message, session); + } + else { + logger.trace("No result object given - no result to handle"); + } + } + + public String getSubscriptionName() { + if (this.delegate instanceof SubscriptionNameProvider) { + return ((SubscriptionNameProvider) this.delegate).getSubscriptionName(); + } + else { + return this.delegate.getClass().getName(); + } + } + + + /** + * 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 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. + *

The default implementation simply returns the configured + * default listener method, if any. + * @param originalMessage the JMS request message + * @param extractedMessage the converted JMS request message, + * to be passed into the listener method as argument + * @return the name of the listener method (never null) + * @throws JMSException if thrown by JMS API methods + * @see #setDefaultListenerMethod + */ + protected String getListenerMethodName(Message originalMessage, Object extractedMessage) throws JMSException { + return getDefaultListenerMethod(); + } + + /** + * Build an array of arguments to be passed into the target listener method. + * Allows for multiple method arguments to be built from a single message object. + *

The default implementation builds an array with the given message object + * as sole element. This means that the extracted message will always be passed + * into a single method argument, even if it is an array, with the target + * method having a corresponding single argument of the array's type declared. + *

This can be overridden to treat special message content such as arrays + * differently, for example passing in each element of the message array + * as distinct method argument. + * @param extractedMessage the content of the message + * @return the array of arguments to be passed into the + * listener method (each element of the array corresponding + * to a distinct method argument) + */ + protected Object[] buildListenerArguments(Object extractedMessage) { + return new Object[] {extractedMessage}; + } + + /** + * Invoke the specified listener method. + * @param methodName the name of the listener method + * @param arguments the message arguments to be passed in + * @return the result returned from the listener method + * @throws JMSException if thrown by JMS API methods + * @see #getListenerMethodName + * @see #buildListenerArguments + */ + protected Object invokeListenerMethod(String methodName, Object[] arguments) throws JMSException { + try { + MethodInvoker methodInvoker = new MethodInvoker(); + methodInvoker.setTargetObject(getDelegate()); + methodInvoker.setTargetMethod(methodName); + methodInvoker.setArguments(arguments); + methodInvoker.prepare(); + return methodInvoker.invoke(); + } + catch (InvocationTargetException ex) { + Throwable targetEx = ex.getTargetException(); + if (targetEx instanceof JMSException) { + throw (JMSException) targetEx; + } + else { + throw new ListenerExecutionFailedException( + "Listener method '" + methodName + "' threw exception", targetEx); + } + } + catch (Throwable ex) { + throw new ListenerExecutionFailedException("Failed to invoke target method '" + methodName + + "' with arguments " + ObjectUtils.nullSafeToString(arguments), ex); + } + } + + + /** + * Handle the given result object returned from the listener method, + * sending a response message back. + * @param result the result object to handle (never null) + * @param request the original request message + * @param session the JMS Session to operate on (may be 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 Message (never 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 null + * it is returned; if it is null, then the configured + * {@link #resolveDefaultResponseDestination default response destination} + * is returned; if this too is 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 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/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter102.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter102.java new file mode 100644 index 00000000000..e7fbb922ecc --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter102.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2007 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.JMSException; +import javax.jms.Message; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.QueueSender; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicPublisher; +import javax.jms.TopicSession; + +import org.springframework.jms.support.JmsUtils; +import org.springframework.jms.support.converter.SimpleMessageConverter102; + +/** + * A {@link MessageListenerAdapter} subclass for the JMS 1.0.2 specification, + * not relying on JMS 1.1 methods like MessageListenerAdapter itself. + * + *

This class can be used for JMS 1.0.2 providers, offering the same facility + * as MessageListenerAdapter does for JMS 1.1 providers. + * + * @author Juergen Hoeller + * @author Rick Evans + * @since 2.0 + */ +public class MessageListenerAdapter102 extends MessageListenerAdapter { + + /** + * Create a new instance of the {@link MessageListenerAdapter102} class + * with the default settings. + */ + public MessageListenerAdapter102() { + } + + /** + * Create a new instance of the {@link MessageListenerAdapter102} class + * for the given delegate. + * @param delegate the target object to delegate message listening to + */ + public MessageListenerAdapter102(Object delegate) { + super(delegate); + } + + + /** + * Initialize the default implementations for the adapter's strategies: + * SimpleMessageConverter102. + * @see #setMessageConverter + * @see org.springframework.jms.support.converter.SimpleMessageConverter102 + */ + protected void initDefaultStrategies() { + setMessageConverter(new SimpleMessageConverter102()); + } + + /** + * Overrides the superclass method to use the JMS 1.0.2 API to send a response. + *

Uses the JMS pub-sub API if the given destination is a topic, + * else uses the JMS queue API. + */ + protected void sendResponse(Session session, Destination destination, Message response) throws JMSException { + MessageProducer producer = null; + try { + if (destination instanceof Topic) { + producer = ((TopicSession) session).createPublisher((Topic) destination); + postProcessProducer(producer, response); + ((TopicPublisher) producer).publish(response); + } + else { + producer = ((QueueSession) session).createSender((Queue) destination); + postProcessProducer(producer, response); + ((QueueSender) producer).send(response); + } + } + finally { + JmsUtils.closeMessageProducer(producer); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/package.html new file mode 100644 index 00000000000..06cee8c46f5 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/adapter/package.html @@ -0,0 +1,9 @@ + + + +Message listener adapter mechanism that delegates to target listener +methods, converting messages to appropriate message content types +(such as String or byte array) that get passed into listener methods. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/DefaultJmsActivationSpecFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/DefaultJmsActivationSpecFactory.java new file mode 100644 index 00000000000..b73aebec343 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/DefaultJmsActivationSpecFactory.java @@ -0,0 +1,181 @@ +/* + * Copyright 2002-2008 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.endpoint; + +import javax.jms.Session; +import javax.resource.spi.ResourceAdapter; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanWrapper; + +/** + * Default implementation of the {@link JmsActivationSpecFactory} interface. + * Supports the standard JMS properties as defined by the JMS 1.5 specification, + * as well as Spring's extended "maxConcurrency" and "prefetchSize" settings + * through autodetection of well-known vendor-specific provider properties. + * + *

An ActivationSpec factory is effectively dependent on the concrete + * JMS provider, e.g. on ActiveMQ. This default implementation simply + * guesses the ActivationSpec class name from the provider's class name + * ("ActiveMQResourceAdapter" -> "ActiveMQActivationSpec" in the same package, + * or "ActivationSpecImpl" in the same package as the ResourceAdapter class), + * and populates the ActivationSpec properties as suggested by the + * JCA 1.5 specification (Appendix B). Specify the 'activationSpecClass' + * property explicitly if these default naming rules do not apply. + * + *

Note: ActiveMQ, JORAM and WebSphere are supported in terms of extended + * settings (through the detection of their bean property naming conventions). + * The default ActivationSpec class detection rules may apply to other + * JMS providers as well. + * + *

Thanks to Agim Emruli and Laurie Chan for pointing out WebSphere MQ + * settings and contributing corresponding tests! + * + * @author Juergen Hoeller + * @since 2.5 + * @see #setActivationSpecClass + */ +public class DefaultJmsActivationSpecFactory extends StandardJmsActivationSpecFactory { + + private static final String RESOURCE_ADAPTER_SUFFIX = "ResourceAdapter"; + + private static final String RESOURCE_ADAPTER_IMPL_SUFFIX = "ResourceAdapterImpl"; + + private static final String ACTIVATION_SPEC_SUFFIX = "ActivationSpec"; + + private static final String ACTIVATION_SPEC_IMPL_SUFFIX = "ActivationSpecImpl"; + + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + + /** + * This implementation guesses the ActivationSpec class name from the + * provider's class name: e.g. "ActiveMQResourceAdapter" -> + * "ActiveMQActivationSpec" in the same package, or a class named + * "ActivationSpecImpl" in the same package as the ResourceAdapter class. + */ + protected Class determineActivationSpecClass(ResourceAdapter adapter) { + String adapterClassName = adapter.getClass().getName(); + + if (adapterClassName.endsWith(RESOURCE_ADAPTER_SUFFIX)) { + // e.g. ActiveMQ + String providerName = + adapterClassName.substring(0, adapterClassName.length() - RESOURCE_ADAPTER_SUFFIX.length()); + String specClassName = providerName + ACTIVATION_SPEC_SUFFIX; + try { + return adapter.getClass().getClassLoader().loadClass(specClassName); + } + catch (ClassNotFoundException ex) { + logger.debug("No default ActivationSpec class found: " + specClassName); + } + } + + else if (adapterClassName.endsWith(RESOURCE_ADAPTER_IMPL_SUFFIX)){ + //e.g. WebSphere + String providerName = + adapterClassName.substring(0, adapterClassName.length() - RESOURCE_ADAPTER_IMPL_SUFFIX.length()); + String specClassName = providerName + ACTIVATION_SPEC_IMPL_SUFFIX; + try { + return adapter.getClass().getClassLoader().loadClass(specClassName); + } + catch (ClassNotFoundException ex) { + logger.debug("No default ActivationSpecImpl class found: " + specClassName); + } + } + + // e.g. JORAM + String providerPackage = adapterClassName.substring(0, adapterClassName.lastIndexOf('.') + 1); + String specClassName = providerPackage + ACTIVATION_SPEC_IMPL_SUFFIX; + try { + return adapter.getClass().getClassLoader().loadClass(specClassName); + } + catch (ClassNotFoundException ex) { + logger.debug("No default ActivationSpecImpl class found in provider package: " + specClassName); + } + + // ActivationSpecImpl class in "inbound" subpackage (WebSphere MQ 6.0.2.1) + specClassName = providerPackage + "inbound." + ACTIVATION_SPEC_IMPL_SUFFIX; + try { + return adapter.getClass().getClassLoader().loadClass(specClassName); + } + catch (ClassNotFoundException ex) { + logger.debug("No default ActivationSpecImpl class found in inbound subpackage: " + specClassName); + } + + throw new IllegalStateException("No ActivationSpec class defined - " + + "specify the 'activationSpecClass' property or override the 'determineActivationSpecClass' method"); + } + + /** + * This implementation supports Spring's extended "maxConcurrency" + * and "prefetchSize" settings through detecting corresponding + * ActivationSpec properties: "maxSessions"/"maxNumberOfWorks" and + * "maxMessagesPerSessions"/"maxMessages", respectively + * (following ActiveMQ's and JORAM's naming conventions). + */ + protected void populateActivationSpecProperties(BeanWrapper bw, JmsActivationSpecConfig config) { + super.populateActivationSpecProperties(bw, config); + if (config.getMaxConcurrency() > 0) { + if (bw.isWritableProperty("maxSessions")) { + // ActiveMQ + bw.setPropertyValue("maxSessions", Integer.toString(config.getMaxConcurrency())); + } + else if (bw.isWritableProperty("maxNumberOfWorks")) { + // JORAM + bw.setPropertyValue("maxNumberOfWorks", Integer.toString(config.getMaxConcurrency())); + } + else if (bw.isWritableProperty("maxConcurrency")){ + // WebSphere + bw.setPropertyValue("maxConcurrency", Integer.toString(config.getMaxConcurrency())); + } + } + if (config.getPrefetchSize() > 0) { + if (bw.isWritableProperty("maxMessagesPerSessions")) { + // ActiveMQ + bw.setPropertyValue("maxMessagesPerSessions", Integer.toString(config.getPrefetchSize())); + } + else if (bw.isWritableProperty("maxMessages")) { + // JORAM + bw.setPropertyValue("maxMessages", Integer.toString(config.getPrefetchSize())); + } + else if(bw.isWritableProperty("maxBatchSize")){ + // WebSphere + bw.setPropertyValue("maxBatchSize", Integer.toString(config.getPrefetchSize())); + } + } + } + + /** + * This implementation maps SESSION_TRANSACTED onto an + * ActivationSpec property named "useRAManagedTransaction", if available + * (following ActiveMQ's naming conventions). + */ + protected void applyAcknowledgeMode(BeanWrapper bw, int ackMode) { + if (ackMode == Session.SESSION_TRANSACTED && bw.isWritableProperty("useRAManagedTransaction")) { + // ActiveMQ + bw.setPropertyValue("useRAManagedTransaction", "true"); + } + else { + super.applyAcknowledgeMode(bw, ackMode); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java new file mode 100644 index 00000000000..f3a9f6f82f9 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java @@ -0,0 +1,128 @@ +/* + * Copyright 2002-2007 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.endpoint; + +import javax.jms.Session; + +/** + * Common configuration object for activating a JMS message endpoint. + * Gets converted into a provider-specific JCA 1.5 ActivationSpec + * object for activating the endpoint. + * + *

Typically used in combination with {@link JmsMessageEndpointManager}, + * but not tied to it. + * + * @author Juergen Hoeller + * @since 2.5 + * @see JmsActivationSpecFactory + * @see JmsMessageEndpointManager#setActivationSpecConfig + * @see javax.resource.spi.ResourceAdapter#endpointActivation + */ +public class JmsActivationSpecConfig { + + private String destinationName; + + private boolean pubSubDomain = false; + + private boolean subscriptionDurable = false; + + private String durableSubscriptionName; + + private String clientId; + + private String messageSelector; + + private int acknowledgeMode = Session.AUTO_ACKNOWLEDGE; + + private int maxConcurrency = -1; + + private int prefetchSize = -1; + + + public void setDestinationName(String destinationName) { + this.destinationName = destinationName; + } + + public String getDestinationName() { + return this.destinationName; + } + + public void setPubSubDomain(boolean pubSubDomain) { + this.pubSubDomain = pubSubDomain; + } + + public boolean isPubSubDomain() { + return this.pubSubDomain; + } + + public void setSubscriptionDurable(boolean subscriptionDurable) { + this.subscriptionDurable = subscriptionDurable; + } + + public boolean isSubscriptionDurable() { + return this.subscriptionDurable; + } + + public void setDurableSubscriptionName(String durableSubscriptionName) { + this.durableSubscriptionName = durableSubscriptionName; + } + + public String getDurableSubscriptionName() { + return this.durableSubscriptionName; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getClientId() { + return this.clientId; + } + + public void setMessageSelector(String messageSelector) { + this.messageSelector = messageSelector; + } + + public String getMessageSelector() { + return this.messageSelector; + } + + public void setAcknowledgeMode(int acknowledgeMode) { + this.acknowledgeMode = acknowledgeMode; + } + + public int getAcknowledgeMode() { + return this.acknowledgeMode; + } + + public void setMaxConcurrency(int maxConcurrency) { + this.maxConcurrency = maxConcurrency; + } + + public int getMaxConcurrency() { + return this.maxConcurrency; + } + + public void setPrefetchSize(int prefetchSize) { + this.prefetchSize = prefetchSize; + } + + public int getPrefetchSize() { + return this.prefetchSize; + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecFactory.java new file mode 100644 index 00000000000..bc4e6124dda --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecFactory.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2007 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.endpoint; + +import javax.resource.spi.ActivationSpec; +import javax.resource.spi.ResourceAdapter; + +/** + * Strategy interface for creating JCA 1.5 ActivationSpec objects + * based on a configured {@link JmsActivationSpecConfig} object. + * + *

JCA 1.5 ActivationSpec objects are typically JavaBeans, but + * unfortunately provider-specific. This strategy interface allows + * for plugging in any JCA-based JMS provider, creating corresponding + * ActivationSpec objects based on common JMS configuration settings. + * + * @author Juergen Hoeller + * @since 2.5 + * @see JmsActivationSpecConfig + * @see JmsMessageEndpointManager#setActivationSpecFactory + * @see javax.resource.spi.ResourceAdapter#endpointActivation + */ +public interface JmsActivationSpecFactory { + + /** + * Create a JCA 1.5 ActivationSpec object based on the given + * {@link JmsActivationSpecConfig} object. + * @param adapter the ResourceAdapter to create an ActivationSpec object for + * @param config the configured object holding common JMS settings + * @return the provider-specific JCA ActivationSpec object, + * representing the same settings + */ + ActivationSpec createActivationSpec(ResourceAdapter adapter, JmsActivationSpecConfig config); + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java new file mode 100644 index 00000000000..ef6ca3ec2f7 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2007 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.endpoint; + +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.resource.ResourceException; +import javax.resource.spi.UnavailableException; + +import org.springframework.jca.endpoint.AbstractMessageEndpointFactory; + +/** + * JMS-specific implementation of the JCA 1.5 + * {@link javax.resource.spi.endpoint.MessageEndpointFactory} interface, + * providing transaction management capabilities for a JMS listener object + * (e.g. a {@link javax.jms.MessageListener} object). + * + *

Uses a static endpoint implementation, simply wrapping the + * specified message listener object and exposing all of its implemented + * interfaces on the endpoint instance. + * + *

Typically used with Spring's {@link JmsMessageEndpointManager}, + * but not tied to it. As a consequence, this endpoint factory could + * also be used with programmatic endpoint management on a native + * {@link javax.resource.spi.ResourceAdapter} instance. + * + * @author Juergen Hoeller + * @since 2.5 + * @see #setMessageListener + * @see #setTransactionManager + * @see JmsMessageEndpointManager + */ +public class JmsMessageEndpointFactory extends AbstractMessageEndpointFactory { + + private MessageListener messageListener; + + + /** + * Set the JMS MessageListener for this endpoint. + */ + public void setMessageListener(MessageListener messageListener) { + this.messageListener = messageListener; + } + + /** + * Creates a concrete JMS message endpoint, internal to this factory. + */ + protected AbstractMessageEndpoint createEndpointInternal() throws UnavailableException { + return new JmsMessageEndpoint(); + } + + + /** + * Private inner class that implements the concrete JMS message endpoint. + */ + private class JmsMessageEndpoint extends AbstractMessageEndpoint implements MessageListener { + + public void onMessage(Message message) { + boolean applyDeliveryCalls = !hasBeforeDeliveryBeenCalled(); + if (applyDeliveryCalls) { + try { + beforeDelivery(null); + } + catch (ResourceException ex) { + throw new JmsResourceException(ex); + } + } + try { + messageListener.onMessage(message); + } + catch (RuntimeException ex) { + onEndpointException(ex); + throw ex; + } + catch (Error err) { + onEndpointException(err); + throw err; + } + finally { + if (applyDeliveryCalls) { + try { + afterDelivery(); + } + catch (ResourceException ex) { + throw new JmsResourceException(ex); + } + } + } + } + + protected ClassLoader getEndpointClassLoader() { + return messageListener.getClass().getClassLoader(); + } + } + + + /** + * Internal exception thrown when a ResourceExeption has been encountered + * during the endpoint invocation. + *

Will only be used if the ResourceAdapter does not invoke the + * endpoint's beforeDelivery and afterDelivery + * directly, leavng it up to the concrete endpoint to apply those - + * and to handle any ResourceExceptions thrown from them. + */ + public static class JmsResourceException extends RuntimeException { + + public JmsResourceException(ResourceException cause) { + super(cause); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java new file mode 100644 index 00000000000..052fe9ec61c --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2007 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.endpoint; + +import javax.jms.MessageListener; +import javax.resource.ResourceException; + +import org.springframework.jca.endpoint.GenericMessageEndpointManager; +import org.springframework.jms.support.destination.DestinationResolver; + +/** + * Extension of the generic JCA 1.5 + * {@link org.springframework.jca.endpoint.GenericMessageEndpointManager}, + * adding JMS-specific support for ActivationSpec configuration. + * + *

Allows for defining a common {@link JmsActivationSpecConfig} object + * that gets converted into a provider-specific JCA 1.5 ActivationSpec + * object for activating the endpoint. + * + *

NOTE: This JCA-based endpoint manager supports standard JMS + * {@link javax.jms.MessageListener} endpoints only. It does not support + * Spring's {@link org.springframework.jms.listener.SessionAwareMessageListener} + * variant, simply because the JCA endpoint management contract does not allow + * for obtaining the current JMS {@link javax.jms.Session}. + * + * @author Juergen Hoeller + * @since 2.5 + * @see javax.jms.MessageListener + * @see #setActivationSpecConfig + * @see JmsActivationSpecConfig + * @see JmsActivationSpecFactory + * @see JmsMessageEndpointFactory + */ +public class JmsMessageEndpointManager extends GenericMessageEndpointManager { + + private final JmsMessageEndpointFactory endpointFactory = new JmsMessageEndpointFactory(); + + private boolean messageListenerSet = false; + + private JmsActivationSpecFactory activationSpecFactory = new DefaultJmsActivationSpecFactory(); + + private JmsActivationSpecConfig activationSpecConfig; + + + /** + * Set the JMS MessageListener for this endpoint. + *

This is a shortcut for configuring a dedicated JmsMessageEndpointFactory. + * @see JmsMessageEndpointFactory#setMessageListener + */ + public void setMessageListener(MessageListener messageListener) { + this.endpointFactory.setMessageListener(messageListener); + this.messageListenerSet = true; + } + + /** + * Set the XA transaction manager to use for wrapping endpoint + * invocations, enlisting the endpoint resource in each such transaction. + *

The passed-in object may be a transaction manager which implements + * Spring's {@link org.springframework.transaction.jta.TransactionFactory} + * interface, or a plain {@link javax.transaction.TransactionManager}. + *

If no transaction manager is specified, the endpoint invocation + * will simply not be wrapped in an XA transaction. Consult your + * resource provider's ActivationSpec documentation for the local + * transaction options of your particular provider. + *

This is a shortcut for configuring a dedicated JmsMessageEndpointFactory. + * @see JmsMessageEndpointFactory#setTransactionManager + */ + public void setTransactionManager(Object transactionManager) { + this.endpointFactory.setTransactionManager(transactionManager); + } + + /** + * Set the factory for concrete JCA 1.5 ActivationSpec objects, + * creating JCA ActivationSpecs based on + * {@link #setActivationSpecConfig JmsActivationSpecConfig} objects. + *

This factory is dependent on the concrete JMS provider, e.g. on ActiveMQ. + * The default implementation simply guesses the ActivationSpec class name + * from the provider's class name (e.g. "ActiveMQResourceAdapter" -> + * "ActiveMQActivationSpec" in the same package), and populates the + * ActivationSpec properties as suggested by the JCA 1.5 specification + * (plus a couple of autodetected vendor-specific properties). + * @see DefaultJmsActivationSpecFactory + */ + public void setActivationSpecFactory(JmsActivationSpecFactory activationSpecFactory) { + this.activationSpecFactory = + (activationSpecFactory != null ? activationSpecFactory : new DefaultJmsActivationSpecFactory()); + } + + /** + * Set the DestinationResolver to use for resolving destination names + * into the JCA 1.5 ActivationSpec "destination" property. + *

If not specified, destination names will simply be passed in as Strings. + * If specified, destination names will be resolved into Destination objects first. + *

Note that a DestinationResolver is usually specified on the JmsActivationSpecFactory + * (see {@link StandardJmsActivationSpecFactory#setDestinationResolver}). This is simply + * a shortcut for parameterizing the default JmsActivationSpecFactory; it will replace + * any custom JmsActivationSpecFactory that might have been set before. + * @see StandardJmsActivationSpecFactory#setDestinationResolver + */ + public void setDestinationResolver(DestinationResolver destinationResolver) { + DefaultJmsActivationSpecFactory factory = new DefaultJmsActivationSpecFactory(); + factory.setDestinationResolver(destinationResolver); + this.activationSpecFactory = factory; + } + + /** + * Specify the {@link JmsActivationSpecConfig} object that this endpoint manager + * should use for activating its listener. + *

This config object will be turned into a concrete JCA 1.5 ActivationSpec + * object through a {@link #setActivationSpecFactory JmsActivationSpecFactory}. + */ + public void setActivationSpecConfig(JmsActivationSpecConfig activationSpecConfig) { + this.activationSpecConfig = activationSpecConfig; + } + + + public void afterPropertiesSet() throws ResourceException { + if (this.messageListenerSet) { + setMessageEndpointFactory(this.endpointFactory); + } + if (this.activationSpecConfig != null) { + setActivationSpec( + this.activationSpecFactory.createActivationSpec(getResourceAdapter(), this.activationSpecConfig)); + } + super.afterPropertiesSet(); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java new file mode 100644 index 00000000000..adbb2153d3c --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java @@ -0,0 +1,204 @@ +/* + * Copyright 2002-2008 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.endpoint; + +import java.util.Properties; + +import javax.jms.JMSException; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.Topic; +import javax.resource.spi.ActivationSpec; +import javax.resource.spi.ResourceAdapter; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.jms.support.destination.DestinationResolutionException; +import org.springframework.jms.support.destination.DestinationResolver; + +/** + * Standard implementation of the {@link JmsActivationSpecFactory} interface. + * Supports the standard JMS properties as defined by the JMS 1.5 specification + * (Appendix B); ignores Spring's "maxConcurrency" and "prefetchSize" settings. + * + *

The 'activationSpecClass' property is required, explicitly defining + * the fully-qualified class name of the provider's ActivationSpec class + * (e.g. "org.apache.activemq.ra.ActiveMQActivationSpec"). + * + *

Check out {@link DefaultJmsActivationSpecFactory} for an extended variant + * of this class, supporting some further default conventions beyond the plain + * JMS 1.5 specification. + * + * @author Juergen Hoeller + * @since 2.5 + * @see #setActivationSpecClass + * @see DefaultJmsActivationSpecFactory + */ +public class StandardJmsActivationSpecFactory implements JmsActivationSpecFactory { + + private Class activationSpecClass; + + private Properties defaultProperties; + + private DestinationResolver destinationResolver; + + + /** + * Specify the fully-qualified ActivationSpec class name for the target + * provider (e.g. "org.apache.activemq.ra.ActiveMQActivationSpec"). + */ + public void setActivationSpecClass(Class activationSpecClass) { + this.activationSpecClass = activationSpecClass; + } + + /** + * Specify custom default properties, with String keys and String values. + *

Applied to each ActivationSpec object before it gets populated with + * listener-specific settings. Allows for configuring vendor-specific properties + * beyond the Spring-defined settings in {@link JmsActivationSpecConfig}. + */ + public void setDefaultProperties(Properties defaultProperties) { + this.defaultProperties = defaultProperties; + } + + /** + * Set the DestinationResolver to use for resolving destination names + * into the JCA 1.5 ActivationSpec "destination" property. + *

If not specified, destination names will simply be passed in as Strings. + * If specified, destination names will be resolved into Destination objects first. + *

Note that a DestinationResolver for use with this factory must be + * able to work without an active JMS Session: e.g. + * {@link org.springframework.jms.support.destination.JndiDestinationResolver} + * or {@link org.springframework.jms.support.destination.BeanFactoryDestinationResolver} + * but not {@link org.springframework.jms.support.destination.DynamicDestinationResolver}. + */ + public void setDestinationResolver(DestinationResolver destinationResolver) { + this.destinationResolver = destinationResolver; + } + + + public ActivationSpec createActivationSpec(ResourceAdapter adapter, JmsActivationSpecConfig config) { + Class activationSpecClassToUse = this.activationSpecClass; + if (activationSpecClassToUse == null) { + activationSpecClassToUse = determineActivationSpecClass(adapter); + if (activationSpecClassToUse == null) { + throw new IllegalStateException("Property 'activationSpecClass' is required"); + } + } + + ActivationSpec spec = (ActivationSpec) BeanUtils.instantiateClass(activationSpecClassToUse); + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(spec); + if (this.defaultProperties != null) { + bw.setPropertyValues(this.defaultProperties); + } + populateActivationSpecProperties(bw, config); + return spec; + } + + /** + * Determine the ActivationSpec class for the given ResourceAdapter, + * if possible. Called if no 'activationSpecClass' has been set explicitly + * @param adapter the ResourceAdapter to check + * @return the corresponding ActivationSpec class, or null + * if not determinable + * @see #setActivationSpecClass + */ + protected Class determineActivationSpecClass(ResourceAdapter adapter) { + return null; + } + + /** + * Populate the given ApplicationSpec object with the settings + * defined in the given configuration object. + *

This implementation applies all standard JMS settings, but ignores + * "maxConcurrency" and "prefetchSize" - not supported in standard JCA 1.5. + * @param bw the BeanWrapper wrapping the ActivationSpec object + * @param config the configured object holding common JMS settings + */ + protected void populateActivationSpecProperties(BeanWrapper bw, JmsActivationSpecConfig config) { + String destinationName = config.getDestinationName(); + boolean pubSubDomain = config.isPubSubDomain(); + Object destination = destinationName; + if (this.destinationResolver != null) { + try { + destination = this.destinationResolver.resolveDestinationName(null, destinationName, pubSubDomain); + } + catch (JMSException ex) { + throw new DestinationResolutionException("Cannot resolve destination name [" + destinationName + "]", ex); + } + } + bw.setPropertyValue("destination", destination); + bw.setPropertyValue("destinationType", pubSubDomain ? Topic.class.getName() : Queue.class.getName()); + + if (bw.isWritableProperty("subscriptionDurability")) { + bw.setPropertyValue("subscriptionDurability", config.isSubscriptionDurable() ? "Durable" : "NonDurable"); + } + else if (config.isSubscriptionDurable()) { + // Standard JCA 1.5 "subscriptionDurability" apparently not supported... + throw new IllegalArgumentException( + "Durable subscriptions not supported by underlying provider: " + this.activationSpecClass.getName()); + } + if (config.getDurableSubscriptionName() != null) { + bw.setPropertyValue("subscriptionName", config.getDurableSubscriptionName()); + } + if (config.getClientId() != null) { + bw.setPropertyValue("clientId", config.getClientId()); + } + + if (config.getMessageSelector() != null) { + bw.setPropertyValue("messageSelector", config.getMessageSelector()); + } + + applyAcknowledgeMode(bw, config.getAcknowledgeMode()); + } + + /** + * Apply the specified acknowledge mode to the ActivationSpec object. + *

This implementation applies the standard JCA 1.5 acknowledge modes + * "Auto-acknowledge" and "Dups-ok-acknowledge". It throws an exception in + * case of CLIENT_ACKNOWLEDGE or SESSION_TRANSACTED + * having been requested. + * @param bw the BeanWrapper wrapping the ActivationSpec object + * @param ackMode the configured acknowledge mode + * (according to the constants in {@link javax.jms.Session} + * @see javax.jms.Session#AUTO_ACKNOWLEDGE + * @see javax.jms.Session#DUPS_OK_ACKNOWLEDGE + * @see javax.jms.Session#CLIENT_ACKNOWLEDGE + * @see javax.jms.Session#SESSION_TRANSACTED + */ + protected void applyAcknowledgeMode(BeanWrapper bw, int ackMode) { + if (ackMode == Session.SESSION_TRANSACTED) { + throw new IllegalArgumentException("No support for SESSION_TRANSACTED: Only \"Auto-acknowledge\" " + + "and \"Dups-ok-acknowledge\" supported in standard JCA 1.5"); + } + else if (ackMode == Session.CLIENT_ACKNOWLEDGE) { + throw new IllegalArgumentException("No support for CLIENT_ACKNOWLEDGE: Only \"Auto-acknowledge\" " + + "and \"Dups-ok-acknowledge\" supported in standard JCA 1.5"); + } + else if (bw.isWritableProperty("acknowledgeMode")) { + bw.setPropertyValue("acknowledgeMode", + ackMode == Session.DUPS_OK_ACKNOWLEDGE ? "Dups-ok-acknowledge" : "Auto-acknowledge"); + } + else if (ackMode == Session.DUPS_OK_ACKNOWLEDGE) { + // Standard JCA 1.5 "acknowledgeMode" apparently not supported (e.g. WebSphere MQ 6.0.2.1) + throw new IllegalArgumentException( + "Dups-ok-acknowledge not supported by underlying provider: " + this.activationSpecClass.getName()); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/package.html new file mode 100644 index 00000000000..ff79cdab57d --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/endpoint/package.html @@ -0,0 +1,7 @@ + + + +This package provides JCA-based endpoint management for JMS message listeners. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/listener/package.html new file mode 100644 index 00000000000..00757137eae --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/package.html @@ -0,0 +1,9 @@ + + + +This package contains the base message listener container facility. +It also offers the DefaultMessageListenerContainer and SimpleMessageListenerContainer +implementations, based on the plain JMS client API. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/AbstractPoolingServerSessionFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/AbstractPoolingServerSessionFactory.java new file mode 100644 index 00000000000..d2ac49c0919 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/AbstractPoolingServerSessionFactory.java @@ -0,0 +1,179 @@ +/* + * Copyright 2002-2008 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.serversession; + +import javax.jms.JMSException; +import javax.jms.ServerSession; +import javax.jms.Session; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.task.TaskExecutor; +import org.springframework.jms.support.JmsUtils; +import org.springframework.scheduling.timer.TimerTaskExecutor; + +/** + * Abstract base class for ServerSessionFactory implementations + * that pool ServerSessionFactory instances. + * + *

Provides a factory method that creates a poolable ServerSession + * (to be added as new instance to a pool), a callback method invoked + * when a ServerSession finished an execution of its listener (to return + * an instance to the pool), and a method to destroy a ServerSession instance + * (after removing an instance from the pool). + * + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of Spring 2.5, in favor of DefaultMessageListenerContainer + * and JmsMessageEndpointManager. To be removed in Spring 3.0. + * @see org.springframework.jms.listener.serversession.CommonsPoolServerSessionFactory + */ +public abstract class AbstractPoolingServerSessionFactory implements ServerSessionFactory { + + protected final Log logger = LogFactory.getLog(getClass()); + + private TaskExecutor taskExecutor; + + private int maxSize; + + + /** + * Specify the TaskExecutor to use for executing ServerSessions + * (and consequently, the underlying MessageListener). + *

Default is a {@link org.springframework.scheduling.timer.TimerTaskExecutor} + * for each pooled ServerSession, using one Thread per pooled JMS Session. + * Alternatives are a shared TimerTaskExecutor, sharing a single Thread + * for the execution of all ServerSessions, or a TaskExecutor + * implementation backed by a thread pool. + */ + public void setTaskExecutor(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + /** + * Return the TaskExecutor to use for executing ServerSessions. + */ + protected TaskExecutor getTaskExecutor() { + return this.taskExecutor; + } + + /** + * Set the maximum size of the pool. + */ + public void setMaxSize(int maxSize) { + this.maxSize = maxSize; + } + + /** + * Return the maximum size of the pool. + */ + public int getMaxSize() { + return this.maxSize; + } + + + /** + * Create a new poolable ServerSession. + * To be called when a new instance should be added to the pool. + * @param sessionManager the listener session manager to create the + * poolable ServerSession for + * @return the new poolable ServerSession + * @throws JMSException if creation failed + */ + protected final ServerSession createServerSession(ListenerSessionManager sessionManager) throws JMSException { + return new PoolableServerSession(sessionManager); + } + + /** + * Destroy the given poolable ServerSession. + * To be called when an instance got removed from the pool. + * @param serverSession the poolable ServerSession to destroy + */ + protected final void destroyServerSession(ServerSession serverSession) { + if (serverSession != null) { + ((PoolableServerSession) serverSession).close(); + } + } + + + /** + * Template method called by a ServerSession if it finished + * execution of its listener and is ready to go back into the pool. + *

Subclasses should implement the actual returning of the instance + * to the pool. + * @param serverSession the ServerSession that finished its execution + * @param sessionManager the session manager that the ServerSession belongs to + */ + protected abstract void serverSessionFinished( + ServerSession serverSession, ListenerSessionManager sessionManager); + + + /** + * ServerSession implementation designed to be pooled. + * Creates a new JMS Session on instantiation, reuses it + * for all executions, and closes it on close. + *

Creates a TimerTaskExecutor (using a single Thread) per + * ServerSession, unless given a specific TaskExecutor to use. + */ + private class PoolableServerSession implements ServerSession { + + private final ListenerSessionManager sessionManager; + + private final Session session; + + private TaskExecutor taskExecutor; + + private TimerTaskExecutor internalExecutor; + + public PoolableServerSession(final ListenerSessionManager sessionManager) throws JMSException { + this.sessionManager = sessionManager; + this.session = sessionManager.createListenerSession(); + this.taskExecutor = getTaskExecutor(); + if (this.taskExecutor == null) { + this.internalExecutor = new TimerTaskExecutor(); + this.internalExecutor.afterPropertiesSet(); + this.taskExecutor = this.internalExecutor; + } + } + + public Session getSession() { + return this.session; + } + + public void start() { + this.taskExecutor.execute(new Runnable() { + public void run() { + try { + sessionManager.executeListenerSession(session); + } + finally { + serverSessionFinished(PoolableServerSession.this, sessionManager); + } + } + }); + } + + public void close() { + if (this.internalExecutor != null) { + this.internalExecutor.destroy(); + } + JmsUtils.closeSession(this.session); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/CommonsPoolServerSessionFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/CommonsPoolServerSessionFactory.java new file mode 100644 index 00000000000..f10dc356da2 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/CommonsPoolServerSessionFactory.java @@ -0,0 +1,274 @@ +/* + * Copyright 2002-2008 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.serversession; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.jms.JMSException; +import javax.jms.ServerSession; + +import org.apache.commons.pool.ObjectPool; +import org.apache.commons.pool.PoolableObjectFactory; +import org.apache.commons.pool.impl.GenericObjectPool; + +/** + * {@link ServerSessionFactory} implementation that holds JMS + * ServerSessions in a configurable Jakarta Commons Pool. + * + *

By default, an instance of GenericObjectPool is created. + * Subclasses may change the type of ObjectPool used by + * overriding the createObjectPool method. + * + *

Provides many configuration properties mirroring those of the Commons Pool + * GenericObjectPool class; these properties are passed to the + * GenericObjectPool during construction. If creating a subclass of this + * class to change the ObjectPool implementation type, pass in the values + * of configuration properties that are relevant to your chosen implementation. + * + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of Spring 2.5, in favor of DefaultMessageListenerContainer + * and JmsMessageEndpointManager. To be removed in Spring 3.0. + * @see GenericObjectPool + * @see #createObjectPool + * @see #setMaxSize + * @see #setMaxIdle + * @see #setMinIdle + * @see #setMaxWait + */ +public class CommonsPoolServerSessionFactory extends AbstractPoolingServerSessionFactory { + + private int maxIdle = GenericObjectPool.DEFAULT_MAX_IDLE; + + private int minIdle = GenericObjectPool.DEFAULT_MIN_IDLE; + + private long maxWait = GenericObjectPool.DEFAULT_MAX_WAIT; + + private long timeBetweenEvictionRunsMillis = GenericObjectPool.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS; + + private long minEvictableIdleTimeMillis = GenericObjectPool.DEFAULT_MIN_EVICTABLE_IDLE_TIME_MILLIS; + + private final Map serverSessionPools = Collections.synchronizedMap(new HashMap(1)); + + + /** + * Create a CommonsPoolServerSessionFactory with default settings. + * Default maximum size of the pool is 8. + * @see #setMaxSize + * @see GenericObjectPool#setMaxActive + */ + public CommonsPoolServerSessionFactory() { + setMaxSize(GenericObjectPool.DEFAULT_MAX_ACTIVE); + } + + + /** + * Set the maximum number of idle ServerSessions in the pool. + * Default is 8. + * @see GenericObjectPool#setMaxIdle + */ + public void setMaxIdle(int maxIdle) { + this.maxIdle = maxIdle; + } + + /** + * Return the maximum number of idle ServerSessions in the pool. + */ + public int getMaxIdle() { + return this.maxIdle; + } + + /** + * Set the minimum number of idle ServerSessions in the pool. + * Default is 0. + * @see GenericObjectPool#setMinIdle + */ + public void setMinIdle(int minIdle) { + this.minIdle = minIdle; + } + + /** + * Return the minimum number of idle ServerSessions in the pool. + */ + public int getMinIdle() { + return this.minIdle; + } + + /** + * Set the maximum waiting time for fetching an ServerSession from the pool. + * Default is -1, waiting forever. + * @see GenericObjectPool#setMaxWait + */ + public void setMaxWait(long maxWait) { + this.maxWait = maxWait; + } + + /** + * Return the maximum waiting time for fetching a ServerSession from the pool. + */ + public long getMaxWait() { + return this.maxWait; + } + + /** + * Set the time between eviction runs that check idle ServerSessions + * whether they have been idle for too long or have become invalid. + * Default is -1, not performing any eviction. + * @see GenericObjectPool#setTimeBetweenEvictionRunsMillis + */ + public void setTimeBetweenEvictionRunsMillis(long timeBetweenEvictionRunsMillis) { + this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis; + } + + /** + * Return the time between eviction runs that check idle ServerSessions. + */ + public long getTimeBetweenEvictionRunsMillis() { + return this.timeBetweenEvictionRunsMillis; + } + + /** + * Set the minimum time that an idle ServerSession can sit in the pool + * before it becomes subject to eviction. Default is 1800000 (30 minutes). + *

Note that eviction runs need to be performed to take this + * setting into effect. + * @see #setTimeBetweenEvictionRunsMillis + * @see GenericObjectPool#setMinEvictableIdleTimeMillis + */ + public void setMinEvictableIdleTimeMillis(long minEvictableIdleTimeMillis) { + this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis; + } + + /** + * Return the minimum time that an idle ServerSession can sit in the pool. + */ + public long getMinEvictableIdleTimeMillis() { + return this.minEvictableIdleTimeMillis; + } + + + /** + * Returns a ServerSession from the pool, creating a new pool for the given + * session manager if necessary. + * @see #createObjectPool + */ + public ServerSession getServerSession(ListenerSessionManager sessionManager) throws JMSException { + ObjectPool pool = null; + synchronized (this.serverSessionPools) { + pool = (ObjectPool) this.serverSessionPools.get(sessionManager); + if (pool == null) { + if (logger.isInfoEnabled()) { + logger.info("Creating Commons ServerSession pool for: " + sessionManager); + } + pool = createObjectPool(sessionManager); + this.serverSessionPools.put(sessionManager, pool); + } + } + try { + return (ServerSession) pool.borrowObject(); + } + catch (Exception ex) { + JMSException jmsEx = new JMSException("Failed to borrow ServerSession from pool"); + jmsEx.setLinkedException(ex); + throw jmsEx; + } + } + + /** + * Subclasses can override this if they want to return a specific Commons pool. + * They should apply any configuration properties to the pool here. + *

Default is a GenericObjectPool instance with the given pool size. + * @param sessionManager the session manager to use for + * creating and executing new listener sessions + * @return an empty Commons ObjectPool. + * @see org.apache.commons.pool.impl.GenericObjectPool + * @see #setMaxSize + */ + protected ObjectPool createObjectPool(ListenerSessionManager sessionManager) { + GenericObjectPool pool = new GenericObjectPool(createPoolableObjectFactory(sessionManager)); + pool.setMaxActive(getMaxSize()); + pool.setMaxIdle(getMaxIdle()); + pool.setMinIdle(getMinIdle()); + pool.setMaxWait(getMaxWait()); + pool.setTimeBetweenEvictionRunsMillis(getTimeBetweenEvictionRunsMillis()); + pool.setMinEvictableIdleTimeMillis(getMinEvictableIdleTimeMillis()); + return pool; + } + + /** + * Create a Commons PoolableObjectFactory adapter for the given session manager. + * Calls createServerSession and destroyServerSession + * as defined by the AbstractPoolingServerSessionFactory class. + * @param sessionManager the session manager to use for + * creating and executing new listener sessions + * @return the Commons PoolableObjectFactory + * @see #createServerSession + * @see #destroyServerSession + */ + protected PoolableObjectFactory createPoolableObjectFactory(final ListenerSessionManager sessionManager) { + return new PoolableObjectFactory() { + public Object makeObject() throws JMSException { + return createServerSession(sessionManager); + } + public void destroyObject(Object obj) { + destroyServerSession((ServerSession) obj); + } + public boolean validateObject(Object obj) { + return true; + } + public void activateObject(Object obj) { + } + public void passivateObject(Object obj) { + } + }; + } + + /** + * Returns the given ServerSession, which just finished an execution + * of its listener, back to the pool. + */ + protected void serverSessionFinished(ServerSession serverSession, ListenerSessionManager sessionManager) { + ObjectPool pool = (ObjectPool) this.serverSessionPools.get(sessionManager); + if (pool == null) { + throw new IllegalStateException("No pool found for session manager [" + sessionManager + "]"); + } + try { + pool.returnObject(serverSession); + } + catch (Exception ex) { + logger.error("Failed to return ServerSession to pool", ex); + } + } + + /** + * Closes and removes the pool for the given session manager. + */ + public void close(ListenerSessionManager sessionManager) { + ObjectPool pool = (ObjectPool) this.serverSessionPools.remove(sessionManager); + if (pool != null) { + try { + pool.close(); + } + catch (Exception ex) { + logger.error("Failed to close ServerSession pool", ex); + } + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ListenerSessionManager.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ListenerSessionManager.java new file mode 100644 index 00000000000..fd39e14e50c --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ListenerSessionManager.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2008 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.serversession; + +import javax.jms.JMSException; +import javax.jms.Session; + +/** + * SPI interface for creating and executing JMS Sessions, + * pre-populated with a specific MessageListener. + * Implemented by ServerSessionMessageListenerContainer, + * accessed by ServerSessionFactory implementations. + * + *

Effectively, an instance that implements this interface + * represents a message listener container for a specific + * listener and destination. + * + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of Spring 2.5, in favor of DefaultMessageListenerContainer + * and JmsMessageEndpointManager. To be removed in Spring 3.0. + * @see ServerSessionFactory + * @see ServerSessionMessageListenerContainer + */ +public interface ListenerSessionManager { + + /** + * Create a new JMS Session, pre-populated with this manager's + * MessageListener. + * @return the new JMS Session + * @throws JMSException if Session creation failed + * @see javax.jms.Session#setMessageListener(javax.jms.MessageListener) + */ + Session createListenerSession() throws JMSException; + + /** + * Execute the given JMS Session, triggering its MessageListener + * with pre-loaded messages. + * @param session the JMS Session to invoke + * @see javax.jms.Session#run() + */ + void executeListenerSession(Session session); + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionFactory.java new file mode 100644 index 00000000000..65f0698ad8b --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionFactory.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2008 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.serversession; + +import javax.jms.JMSException; +import javax.jms.ServerSession; + +import org.springframework.jms.listener.serversession.ListenerSessionManager; + +/** + * SPI interface to be implemented by components that manage + * JMS ServerSessions. Usually, but not necessarily, an implementation + * of this interface will hold a pool of ServerSessions. + * + *

The passed-in ListenerSessionManager has to be used for creating + * and executing JMS Sessions. This session manager is responsible for + * registering a MessageListener with all Sessions that it creates. + * Consequently, the ServerSessionFactory implementation has to + * concentrate on the actual lifecycle (e.g. pooling) of JMS Sessions, + * but is not concerned about Session creation or execution. + * + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of Spring 2.5, in favor of DefaultMessageListenerContainer + * and JmsMessageEndpointManager. To be removed in Spring 3.0. + * @see org.springframework.jms.listener.serversession.ListenerSessionManager + * @see org.springframework.jms.listener.serversession.ServerSessionMessageListenerContainer + */ +public interface ServerSessionFactory { + + /** + * Retrieve a JMS ServerSession for the given session manager. + * @param sessionManager the session manager to use for + * creating and executing new listener sessions + * (implicitly indicating the target listener to invoke) + * @return the JMS ServerSession + * @throws JMSException if retrieval failed + */ + ServerSession getServerSession(ListenerSessionManager sessionManager) throws JMSException; + + /** + * Close all ServerSessions for the given session manager. + * @param sessionManager the session manager used for + * creating and executing new listener sessions + * (implicitly indicating the target listener) + */ + void close(ListenerSessionManager sessionManager); + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionMessageListenerContainer.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionMessageListenerContainer.java new file mode 100644 index 00000000000..fc12215bf5e --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionMessageListenerContainer.java @@ -0,0 +1,256 @@ +/* + * Copyright 2002-2008 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.serversession; + +import javax.jms.Connection; +import javax.jms.ConnectionConsumer; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.jms.ServerSession; +import javax.jms.ServerSessionPool; +import javax.jms.Session; +import javax.jms.Topic; + +import org.springframework.jms.listener.AbstractMessageListenerContainer; +import org.springframework.jms.support.JmsUtils; + +/** + * Message listener container that builds on the {@link javax.jms.ServerSessionPool} + * SPI, creating JMS ServerSession instances through a pluggable + * {@link ServerSessionFactory}. + * + *

NOTE: This class requires a JMS 1.1+ provider, because it builds on the + * domain-independent API. Use the {@link ServerSessionMessageListenerContainer102} + * subclass for a JMS 1.0.2 provider, e.g. when running on a J2EE 1.3 server. + * + *

The default ServerSessionFactory is a {@link SimpleServerSessionFactory}, + * which will create a new ServerSession for each listener execution. + * Consider specifying a {@link CommonsPoolServerSessionFactory} to reuse JMS + * Sessions and/or to limit the number of concurrent ServerSession executions. + * + *

See the {@link AbstractMessageListenerContainer} javadoc for details + * on acknowledge modes and other configuration options. + * + *

This is an 'advanced' (special-purpose) message listener container. + * For a simpler message listener container, in particular when using + * a JMS provider without ServerSessionPool support, consider using + * {@link org.springframework.jms.listener.SimpleMessageListenerContainer}. + * For a general one-stop shop that is nevertheless very flexible, consider + * {@link org.springframework.jms.listener.DefaultMessageListenerContainer}. + * + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of Spring 2.5, in favor of DefaultMessageListenerContainer + * and JmsMessageEndpointManager. To be removed in Spring 3.0. + * @see org.springframework.jms.listener.SimpleMessageListenerContainer + * @see org.springframework.jms.listener.endpoint.JmsMessageEndpointManager + */ +public class ServerSessionMessageListenerContainer extends AbstractMessageListenerContainer + implements ListenerSessionManager { + + private ServerSessionFactory serverSessionFactory = new SimpleServerSessionFactory(); + + private int maxMessagesPerTask = 1; + + private ConnectionConsumer consumer; + + + /** + * Set the Spring ServerSessionFactory to use. + *

Default is a plain SimpleServerSessionFactory. + * Consider using a CommonsPoolServerSessionFactory to reuse JMS Sessions + * and/or to limit the number of concurrent ServerSession executions. + * @see SimpleServerSessionFactory + * @see CommonsPoolServerSessionFactory + */ + public void setServerSessionFactory(ServerSessionFactory serverSessionFactory) { + this.serverSessionFactory = + (serverSessionFactory != null ? serverSessionFactory : new SimpleServerSessionFactory()); + } + + /** + * Return the Spring ServerSessionFactory to use. + */ + protected ServerSessionFactory getServerSessionFactory() { + return this.serverSessionFactory; + } + + /** + * Set the maximum number of messages to load into a JMS Session. + * Default is 1. + *

See the corresponding JMS createConnectionConsumer + * argument for details. + * @see javax.jms.Connection#createConnectionConsumer + */ + public void setMaxMessagesPerTask(int maxMessagesPerTask) { + this.maxMessagesPerTask = maxMessagesPerTask; + } + + /** + * Return the maximum number of messages to load into a JMS Session. + */ + protected int getMaxMessagesPerTask() { + return this.maxMessagesPerTask; + } + + + //------------------------------------------------------------------------- + // Implementation of AbstractMessageListenerContainer's template methods + //------------------------------------------------------------------------- + + /** + * Always use a shared JMS Connection. + */ + protected final boolean sharedConnectionEnabled() { + return true; + } + + /** + * Creates a JMS ServerSessionPool for the specified listener and registers + * it with a JMS ConnectionConsumer for the specified destination. + * @see #createServerSessionPool + * @see #createConsumer + */ + protected void doInitialize() throws JMSException { + establishSharedConnection(); + + Connection con = getSharedConnection(); + Destination destination = getDestination(); + if (destination == null) { + Session session = createSession(con); + try { + destination = resolveDestinationName(session, getDestinationName()); + } + finally { + JmsUtils.closeSession(session); + } + } + ServerSessionPool pool = createServerSessionPool(); + this.consumer = createConsumer(con, destination, pool); + } + + /** + * Create a JMS ServerSessionPool for the specified message listener, + * via this container's ServerSessionFactory. + *

This message listener container implements the ListenerSessionManager + * interface, hence can be passed to the ServerSessionFactory itself. + * @return the ServerSessionPool + * @throws JMSException if creation of the ServerSessionPool failed + * @see #setServerSessionFactory + * @see ServerSessionFactory#getServerSession(ListenerSessionManager) + */ + protected ServerSessionPool createServerSessionPool() throws JMSException { + return new ServerSessionPool() { + public ServerSession getServerSession() throws JMSException { + logger.debug("JMS ConnectionConsumer requests ServerSession"); + return getServerSessionFactory().getServerSession(ServerSessionMessageListenerContainer.this); + } + }; + } + + /** + * Return the JMS ConnectionConsumer used by this message listener container. + * Available after initialization. + */ + protected final ConnectionConsumer getConsumer() { + return this.consumer; + } + + /** + * Close the JMS ServerSessionPool for the specified message listener, + * via this container's ServerSessionFactory, and subsequently also + * this container's JMS ConnectionConsumer. + *

This message listener container implements the ListenerSessionManager + * interface, hence can be passed to the ServerSessionFactory itself. + * @see #setServerSessionFactory + * @see ServerSessionFactory#getServerSession(ListenerSessionManager) + */ + protected void doShutdown() throws JMSException { + logger.debug("Closing ServerSessionFactory"); + getServerSessionFactory().close(this); + logger.debug("Closing JMS ConnectionConsumer"); + this.consumer.close(); + } + + + //------------------------------------------------------------------------- + // Implementation of the ListenerSessionManager interface + //------------------------------------------------------------------------- + + /** + * Create a JMS Session with the specified listener registered. + * Listener execution is delegated to the executeListener method. + *

Default implementation simply calls setMessageListener + * on a newly created JMS Session, according to the JMS specification's + * ServerSessionPool section. + * @return the JMS Session + * @throws JMSException if thrown by JMS API methods + * @see #executeListener + */ + public Session createListenerSession() throws JMSException { + final Session session = createSession(getSharedConnection()); + + session.setMessageListener(new MessageListener() { + public void onMessage(Message message) { + executeListener(session, message); + } + }); + + return session; + } + + /** + * Execute the given JMS Session, triggering invocation + * of its listener. + *

Default implementation simply calls run() + * on the JMS Session, according to the JMS specification's + * ServerSessionPool section. + * @param session the JMS Session to execute + */ + public void executeListenerSession(Session session) { + session.run(); + } + + + //------------------------------------------------------------------------- + // JMS 1.1 factory methods, potentially overridden for JMS 1.0.2 + //------------------------------------------------------------------------- + + /** + * Create a JMS ConnectionConsumer for the given Connection. + *

This implementation uses JMS 1.1 API. + * @param con the JMS Connection to create a Session for + * @param destination the JMS Destination to listen to + * @param pool the ServerSessionpool to use + * @return the new JMS Session + * @throws JMSException if thrown by JMS API methods + */ + protected ConnectionConsumer createConsumer(Connection con, Destination destination, ServerSessionPool pool) + throws JMSException { + + if (isSubscriptionDurable() && destination instanceof Topic) { + return con.createDurableConnectionConsumer( + (Topic) destination, getDurableSubscriptionName(), getMessageSelector(), pool, getMaxMessagesPerTask()); + } + else { + return con.createConnectionConsumer(destination, getMessageSelector(), pool, getMaxMessagesPerTask()); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionMessageListenerContainer102.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionMessageListenerContainer102.java new file mode 100644 index 00000000000..fa4d80216e2 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/ServerSessionMessageListenerContainer102.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2008 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.serversession; + +import javax.jms.Connection; +import javax.jms.ConnectionConsumer; +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Queue; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.ServerSessionPool; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicConnection; +import javax.jms.TopicConnectionFactory; + +/** + * A subclass of {@link ServerSessionMessageListenerContainer} for the JMS 1.0.2 specification, + * not relying on JMS 1.1 methods like ServerSessionMessageListenerContainer itself. + * + *

This class can be used for JMS 1.0.2 providers, offering the same facility as + * ServerSessionMessageListenerContainer does for JMS 1.1 providers. + * + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of Spring 2.5, in favor of DefaultMessageListenerContainer + * and JmsMessageEndpointManager. To be removed in Spring 3.0. + */ +public class ServerSessionMessageListenerContainer102 extends ServerSessionMessageListenerContainer { + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Connection createConnection() throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnectionFactory) getConnectionFactory()).createTopicConnection(); + } + else { + return ((QueueConnectionFactory) getConnectionFactory()).createQueueConnection(); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected ConnectionConsumer createConsumer(Connection con, Destination destination, ServerSessionPool pool) + throws JMSException { + + if (isPubSubDomain()) { + if (isSubscriptionDurable()) { + return ((TopicConnection) con).createDurableConnectionConsumer( + (Topic) destination, getDurableSubscriptionName(), getMessageSelector(), pool, getMaxMessagesPerTask()); + } + else { + return ((TopicConnection) con).createConnectionConsumer( + (Topic) destination, getMessageSelector(), pool, getMaxMessagesPerTask()); + } + } + else { + return ((QueueConnection) con).createConnectionConsumer( + (Queue) destination, getMessageSelector(), pool, getMaxMessagesPerTask()); + } + } + + /** + * This implementation overrides the superclass method to use JMS 1.0.2 API. + */ + protected Session createSession(Connection con) throws JMSException { + if (isPubSubDomain()) { + return ((TopicConnection) con).createTopicSession(isSessionTransacted(), getSessionAcknowledgeMode()); + } + else { + return ((QueueConnection) con).createQueueSession(isSessionTransacted(), getSessionAcknowledgeMode()); + } + } + + /** + * This implementation overrides the superclass method to avoid using + * JMS 1.1's Session getAcknowledgeMode() method. + * The best we can do here is to check the setting on the listener container. + */ + protected boolean isClientAcknowledge(Session session) throws JMSException { + return (getSessionAcknowledgeMode() == Session.CLIENT_ACKNOWLEDGE); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/SimpleServerSessionFactory.java b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/SimpleServerSessionFactory.java new file mode 100644 index 00000000000..0f7b8ec2218 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/SimpleServerSessionFactory.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2008 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.serversession; + +import javax.jms.JMSException; +import javax.jms.ServerSession; +import javax.jms.Session; + +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.core.task.TaskExecutor; +import org.springframework.jms.support.JmsUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * The simplest possible implementation of the ServerSessionFactory SPI: + * creating a new ServerSession with a new JMS Session every time. + * This is the default used by ServerSessionMessageListenerContainer. + * + *

The execution of a ServerSession (and its MessageListener) gets delegated + * to a TaskExecutor. By default, a SimpleAsyncTaskExecutor will be used, + * creating a new Thread for every execution attempt. Alternatives are a + * TimerTaskExecutor, sharing a single Thread for the execution of all + * ServerSessions, or a TaskExecutor implementation backed by a thread pool. + * + *

To reuse JMS Sessions and/or to limit the number of concurrent + * ServerSession executions, consider using a pooling ServerSessionFactory: + * for example, CommonsPoolServerSessionFactory. + * + * @author Juergen Hoeller + * @since 2.0 + * @deprecated as of Spring 2.5, in favor of DefaultMessageListenerContainer + * and JmsMessageEndpointManager. To be removed in Spring 3.0. + * @see org.springframework.core.task.TaskExecutor + * @see org.springframework.core.task.SimpleAsyncTaskExecutor + * @see org.springframework.scheduling.timer.TimerTaskExecutor + * @see CommonsPoolServerSessionFactory + * @see ServerSessionMessageListenerContainer + */ +public class SimpleServerSessionFactory implements ServerSessionFactory { + + /** + * Default thread name prefix: "SimpleServerSessionFactory-". + */ + public static final String DEFAULT_THREAD_NAME_PREFIX = + ClassUtils.getShortName(SimpleServerSessionFactory.class) + "-"; + + + private TaskExecutor taskExecutor = new SimpleAsyncTaskExecutor(DEFAULT_THREAD_NAME_PREFIX); + + + /** + * Specify the TaskExecutor to use for executing ServerSessions + * (and consequently, the underlying MessageListener). + *

Default is a SimpleAsyncTaskExecutor, creating a new Thread for + * every execution attempt. Alternatives are a TimerTaskExecutor, + * sharing a single Thread for the execution of all ServerSessions, + * or a TaskExecutor implementation backed by a thread pool. + * @see org.springframework.core.task.SimpleAsyncTaskExecutor + * @see org.springframework.scheduling.timer.TimerTaskExecutor + */ + public void setTaskExecutor(TaskExecutor taskExecutor) { + Assert.notNull(taskExecutor, "taskExecutor is required"); + this.taskExecutor = taskExecutor; + } + + /** + * Return the TaskExecutor to use for executing ServerSessions. + */ + protected TaskExecutor getTaskExecutor() { + return taskExecutor; + } + + + /** + * Creates a new SimpleServerSession with a new JMS Session + * for every call. + */ + public ServerSession getServerSession(ListenerSessionManager sessionManager) throws JMSException { + return new SimpleServerSession(sessionManager); + } + + /** + * This implementation is empty, as there is no state held for + * each ListenerSessionManager. + */ + public void close(ListenerSessionManager sessionManager) { + } + + + /** + * ServerSession implementation that simply creates a new + * JMS Session and executes it via the specified TaskExecutor. + */ + private class SimpleServerSession implements ServerSession { + + private final ListenerSessionManager sessionManager; + + private final Session session; + + public SimpleServerSession(ListenerSessionManager sessionManager) throws JMSException { + this.sessionManager = sessionManager; + this.session = sessionManager.createListenerSession(); + } + + public Session getSession() { + return session; + } + + public void start() { + getTaskExecutor().execute(new Runnable() { + public void run() { + try { + sessionManager.executeListenerSession(session); + } + finally { + JmsUtils.closeSession(session); + } + } + }); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/package.html new file mode 100644 index 00000000000..31c2c4be21c --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/listener/serversession/package.html @@ -0,0 +1,8 @@ + + + +This package contains the ServerSessionMessageListenerContainer implementation, +based on the standard JMS ServerSessionPool API. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/package.html new file mode 100644 index 00000000000..de0e0309613 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/package.html @@ -0,0 +1,8 @@ + + + +This package contains integration classes for JMS, +allowing for Spring-style JMS access. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerClientInterceptor.java b/org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerClientInterceptor.java new file mode 100644 index 00000000000..6989ea28e92 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerClientInterceptor.java @@ -0,0 +1,442 @@ +/* + * Copyright 2002-2007 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.remoting; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageConsumer; +import javax.jms.MessageFormatException; +import javax.jms.MessageProducer; +import javax.jms.Queue; +import javax.jms.QueueConnection; +import javax.jms.QueueConnectionFactory; +import javax.jms.QueueSender; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.TemporaryQueue; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jms.connection.ConnectionFactoryUtils; +import org.springframework.jms.support.JmsUtils; +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.remoting.RemoteAccessException; +import org.springframework.remoting.RemoteInvocationFailureException; +import org.springframework.remoting.support.DefaultRemoteInvocationFactory; +import org.springframework.remoting.support.RemoteInvocation; +import org.springframework.remoting.support.RemoteInvocationFactory; +import org.springframework.remoting.support.RemoteInvocationResult; + +/** + * {@link org.aopalliance.intercept.MethodInterceptor} for accessing a + * JMS-based remote service. + * + *

Serializes remote invocation objects and deserializes remote invocation + * result objects. Uses Java serialization just like RMI, but with the JMS + * provider as communication infrastructure. + * + *

To be configured with a {@link javax.jms.QueueConnectionFactory} and a + * target queue (either as {@link javax.jms.Queue} reference or as queue name). + * + *

Thanks to James Strachan for the original prototype that this + * JMS invoker mechanism was inspired by! + * + * @author Juergen Hoeller + * @author James Strachan + * @since 2.0 + * @see #setConnectionFactory + * @see #setQueue + * @see #setQueueName + * @see org.springframework.jms.remoting.JmsInvokerServiceExporter + * @see org.springframework.jms.remoting.JmsInvokerProxyFactoryBean + */ +public class JmsInvokerClientInterceptor implements MethodInterceptor, InitializingBean { + + private ConnectionFactory connectionFactory; + + private Object queue; + + private DestinationResolver destinationResolver = new DynamicDestinationResolver(); + + private RemoteInvocationFactory remoteInvocationFactory = new DefaultRemoteInvocationFactory(); + + private MessageConverter messageConverter = new SimpleMessageConverter(); + + private long receiveTimeout = 0; + + + /** + * Set the QueueConnectionFactory to use for obtaining JMS QueueConnections. + */ + public void setConnectionFactory(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + /** + * Return the QueueConnectionFactory to use for obtaining JMS QueueConnections. + */ + protected ConnectionFactory getConnectionFactory() { + return this.connectionFactory; + } + + /** + * Set the target Queue to send invoker requests to. + */ + public void setQueue(Queue queue) { + this.queue = queue; + } + + /** + * Set the name of target queue to send invoker requests to. + * The specified name will be dynamically resolved via the + * {@link #setDestinationResolver DestinationResolver}. + */ + public void setQueueName(String queueName) { + this.queue = queueName; + } + + /** + * Set the DestinationResolver that is to be used to resolve Queue + * references for this accessor. + *

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) { + this.destinationResolver = + (destinationResolver != null ? destinationResolver : new DynamicDestinationResolver()); + } + + /** + * Set the RemoteInvocationFactory to use for this accessor. + * Default is a {@link org.springframework.remoting.support.DefaultRemoteInvocationFactory}. + *

A custom invocation factory can add further context information + * to the invocation, for example user credentials. + */ + public void setRemoteInvocationFactory(RemoteInvocationFactory remoteInvocationFactory) { + this.remoteInvocationFactory = + (remoteInvocationFactory != null ? remoteInvocationFactory : new DefaultRemoteInvocationFactory()); + } + + /** + * Specify the MessageConverter to use for turning + * {@link org.springframework.remoting.support.RemoteInvocation} + * objects into request messages, as well as response messages into + * {@link org.springframework.remoting.support.RemoteInvocationResult} objects. + *

Default is a {@link org.springframework.jms.support.converter.SimpleMessageConverter}, + * using a standard JMS {@link javax.jms.ObjectMessage} for each invocation / + * invocation result object. + *

Custom implementations may generally adapt Serializables into + * special kinds of messages, or might be specifically tailored for + * translating RemoteInvocation(Result)s into specific kinds of messages. + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = (messageConverter != null ? messageConverter : new SimpleMessageConverter()); + } + + /** + * Set the timeout to use for receiving the response message for a request + * (in milliseconds). + *

The default is 0, which indicates a blocking receive without timeout. + * @see javax.jms.MessageConsumer#receive(long) + * @see javax.jms.MessageConsumer#receive() + */ + public void setReceiveTimeout(long receiveTimeout) { + this.receiveTimeout = receiveTimeout; + } + + /** + * Return the timeout to use for receiving the response message for a request + * (in milliseconds). + */ + protected long getReceiveTimeout() { + return this.receiveTimeout; + } + + + public void afterPropertiesSet() { + if (getConnectionFactory() == null) { + throw new IllegalArgumentException("Property 'connectionFactory' is required"); + } + if (this.queue == null) { + throw new IllegalArgumentException("'queue' or 'queueName' is required"); + } + } + + + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + if (AopUtils.isToStringMethod(methodInvocation.getMethod())) { + return "JMS invoker proxy for queue [" + this.queue + "]"; + } + + RemoteInvocation invocation = createRemoteInvocation(methodInvocation); + RemoteInvocationResult result = null; + try { + result = executeRequest(invocation); + } + catch (JMSException ex) { + throw convertJmsInvokerAccessException(ex); + } + try { + return recreateRemoteInvocationResult(result); + } + catch (Throwable ex) { + if (result.hasInvocationTargetException()) { + throw ex; + } + else { + throw new RemoteInvocationFailureException("Invocation of method [" + methodInvocation.getMethod() + + "] failed in JMS invoker remote service at queue [" + this.queue + "]", ex); + } + } + } + + /** + * Create a new RemoteInvocation object for the given AOP method invocation. + * The default implementation delegates to the RemoteInvocationFactory. + *

Can be overridden in subclasses to provide custom RemoteInvocation + * subclasses, containing additional invocation parameters like user credentials. + * Note that it is preferable to use a custom RemoteInvocationFactory which + * is a reusable strategy. + * @param methodInvocation the current AOP method invocation + * @return the RemoteInvocation object + * @see RemoteInvocationFactory#createRemoteInvocation + */ + protected RemoteInvocation createRemoteInvocation(MethodInvocation methodInvocation) { + return this.remoteInvocationFactory.createRemoteInvocation(methodInvocation); + } + + /** + * Execute the given remote invocation, sending an invoker request message + * to this accessor's target queue and waiting for a corresponding response. + * @param invocation the RemoteInvocation to execute + * @return the RemoteInvocationResult object + * @throws JMSException in case of JMS failure + * @see #doExecuteRequest + */ + protected RemoteInvocationResult executeRequest(RemoteInvocation invocation) throws JMSException { + Connection con = createConnection(); + Session session = null; + try { + session = createSession(con); + Queue queueToUse = resolveQueue(session); + Message requestMessage = createRequestMessage(session, invocation); + con.start(); + Message responseMessage = doExecuteRequest(session, queueToUse, requestMessage); + return extractInvocationResult(responseMessage); + } + finally { + JmsUtils.closeSession(session); + ConnectionFactoryUtils.releaseConnection(con, getConnectionFactory(), true); + } + } + + /** + * Create a new JMS Connection for this JMS invoker, + * ideally a javax.jms.QueueConnection. + *

The default implementation uses the + * javax.jms.QueueConnectionFactory API if available, + * falling back to a standard JMS 1.1 ConnectionFactory otherwise. + * This is necessary for working with generic JMS 1.1 connection pools + * (such as ActiveMQ's org.apache.activemq.pool.PooledConnectionFactory). + */ + protected Connection createConnection() throws JMSException { + ConnectionFactory cf = getConnectionFactory(); + if (cf instanceof QueueConnectionFactory) { + return ((QueueConnectionFactory) cf).createQueueConnection(); + } + else { + return cf.createConnection(); + } + } + + /** + * Create a new JMS Session for this JMS invoker, + * ideally a javax.jms.QueueSession. + *

The default implementation uses the + * javax.jms.QueueConnection API if available, + * falling back to a standard JMS 1.1 Connection otherwise. + * This is necessary for working with generic JMS 1.1 connection pools + * (such as ActiveMQ's org.apache.activemq.pool.PooledConnectionFactory). + */ + protected Session createSession(Connection con) throws JMSException { + if (con instanceof QueueConnection) { + return ((QueueConnection) con).createQueueSession(false, Session.AUTO_ACKNOWLEDGE); + } + else { + return con.createSession(false, Session.AUTO_ACKNOWLEDGE); + } + } + + /** + * Resolve this accessor's target queue. + * @param session the current JMS Session + * @return the resolved target Queue + * @throws JMSException if resolution failed + */ + protected Queue resolveQueue(Session session) throws JMSException { + if (this.queue instanceof Queue) { + return (Queue) this.queue; + } + else if (this.queue instanceof String) { + return resolveQueueName(session, (String) this.queue); + } + else { + throw new javax.jms.IllegalStateException( + "Queue object [" + this.queue + "] is neither a [javax.jms.Queue] nor a queue name String"); + } + } + + /** + * Resolve the given queue name into a JMS {@link javax.jms.Queue}, + * via this accessor's {@link DestinationResolver}. + * @param session the current JMS Session + * @param queueName the name of the queue + * @return the located Queue + * @throws JMSException if resolution failed + * @see #setDestinationResolver + */ + protected Queue resolveQueueName(Session session, String queueName) throws JMSException { + return (Queue) this.destinationResolver.resolveDestinationName(session, queueName, false); + } + + /** + * Create the invoker request message. + *

The default implementation creates a JMS ObjectMessage + * for the given RemoteInvocation object. + * @param session the current JMS Session + * @param invocation the remote invocation to send + * @return the JMS Message to send + * @throws JMSException if the message could not be created + */ + protected Message createRequestMessage(Session session, RemoteInvocation invocation) throws JMSException { + return this.messageConverter.toMessage(invocation, session); + } + + /** + * Actually execute the given request, sending the invoker request message + * to the specified target queue and waiting for a corresponding response. + *

The default implementation is based on standard JMS send/receive, + * using a {@link javax.jms.TemporaryQueue} for receiving the response. + * @param session the JMS Session to use + * @param queue the resolved target Queue to send to + * @param requestMessage the JMS Message to send + * @return the RemoteInvocationResult object + * @throws JMSException in case of JMS failure + */ + protected Message doExecuteRequest(Session session, Queue queue, Message requestMessage) throws JMSException { + TemporaryQueue responseQueue = null; + MessageProducer producer = null; + MessageConsumer consumer = null; + try { + if (session instanceof QueueSession) { + // Perform all calls on QueueSession reference for JMS 1.0.2 compatibility... + QueueSession queueSession = (QueueSession) session; + responseQueue = queueSession.createTemporaryQueue(); + QueueSender sender = queueSession.createSender(queue); + producer = sender; + consumer = queueSession.createReceiver(responseQueue); + requestMessage.setJMSReplyTo(responseQueue); + sender.send(requestMessage); + } + else { + // Standard JMS 1.1 API usage... + responseQueue = session.createTemporaryQueue(); + producer = session.createProducer(queue); + consumer = session.createConsumer(responseQueue); + requestMessage.setJMSReplyTo(responseQueue); + producer.send(requestMessage); + } + long timeout = getReceiveTimeout(); + return (timeout > 0 ? consumer.receive(timeout) : consumer.receive()); + } + finally { + JmsUtils.closeMessageConsumer(consumer); + JmsUtils.closeMessageProducer(producer); + if (responseQueue != null) { + responseQueue.delete(); + } + } + } + + /** + * Extract the invocation result from the response message. + *

The default implementation expects a JMS ObjectMessage carrying + * a RemoteInvocationResult object. If an invalid response message is + * encountered, the onInvalidResponse callback gets invoked. + * @param responseMessage the response message + * @return the invocation result + * @throws JMSException is thrown if a JMS exception occurs + * @see #onInvalidResponse + */ + protected RemoteInvocationResult extractInvocationResult(Message responseMessage) throws JMSException { + Object content = this.messageConverter.fromMessage(responseMessage); + if (content instanceof RemoteInvocationResult) { + return (RemoteInvocationResult) content; + } + return onInvalidResponse(responseMessage); + } + + /** + * Callback that is invoked by extractInvocationResult + * when it encounters an invalid response message. + *

The default implementation throws a MessageFormatException. + * @param responseMessage the invalid response message + * @return an alternative invocation result that should be + * returned to the caller (if desired) + * @throws JMSException if the invalid response should lead + * to an infrastructure exception propagated to the caller + * @see #extractInvocationResult + */ + protected RemoteInvocationResult onInvalidResponse(Message responseMessage) throws JMSException { + throw new MessageFormatException("Invalid response message: " + responseMessage); + } + + /** + * Recreate the invocation result contained in the given RemoteInvocationResult + * object. The default implementation calls the default recreate method. + *

Can be overridden in subclass to provide custom recreation, potentially + * processing the returned result object. + * @param result the RemoteInvocationResult to recreate + * @return a return value if the invocation result is a successful return + * @throws Throwable if the invocation result is an exception + * @see org.springframework.remoting.support.RemoteInvocationResult#recreate() + */ + protected Object recreateRemoteInvocationResult(RemoteInvocationResult result) throws Throwable { + return result.recreate(); + } + + /** + * Convert the given JMS invoker access exception to an appropriate + * Spring RemoteAccessException. + * @param ex the exception to convert + * @return the RemoteAccessException to throw + */ + protected RemoteAccessException convertJmsInvokerAccessException(JMSException ex) { + throw new RemoteAccessException("Could not access JMS invoker queue [" + this.queue + "]", ex); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerProxyFactoryBean.java b/org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerProxyFactoryBean.java new file mode 100644 index 00000000000..a6fc5e9c91d --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerProxyFactoryBean.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2007 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.remoting; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.util.ClassUtils; + +/** + * FactoryBean for JMS invoker proxies. Exposes the proxied service for use + * as a bean reference, using the specified service interface. + * + *

Serializes remote invocation objects and deserializes remote invocation + * result objects. Uses Java serialization just like RMI, but with the JMS + * provider as communication infrastructure. + * + *

To be configured with a {@link javax.jms.QueueConnectionFactory} and a + * target queue (either as {@link javax.jms.Queue} reference or as queue name). + * + * @author Juergen Hoeller + * @since 2.0 + * @see #setConnectionFactory + * @see #setQueueName + * @see #setServiceInterface + * @see org.springframework.jms.remoting.JmsInvokerClientInterceptor + * @see org.springframework.jms.remoting.JmsInvokerServiceExporter + */ +public class JmsInvokerProxyFactoryBean extends JmsInvokerClientInterceptor + implements FactoryBean, BeanClassLoaderAware { + + private Class serviceInterface; + + private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + + private Object serviceProxy; + + + /** + * Set the interface that the proxy must implement. + * @param serviceInterface the interface that the proxy must implement + * @throws IllegalArgumentException if the supplied serviceInterface + * is null, or if the supplied serviceInterface + * is not an interface type + */ + public void setServiceInterface(Class serviceInterface) { + if (serviceInterface == null || !serviceInterface.isInterface()) { + throw new IllegalArgumentException("'serviceInterface' must be an interface"); + } + this.serviceInterface = serviceInterface; + } + + public void setBeanClassLoader(ClassLoader classLoader) { + this.beanClassLoader = classLoader; + } + + public void afterPropertiesSet() { + super.afterPropertiesSet(); + if (this.serviceInterface == null) { + throw new IllegalArgumentException("Property 'serviceInterface' is required"); + } + this.serviceProxy = new ProxyFactory(this.serviceInterface, this).getProxy(this.beanClassLoader); + } + + + public Object getObject() { + return this.serviceProxy; + } + + public Class getObjectType() { + return this.serviceInterface; + } + + public boolean isSingleton() { + return true; + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerServiceExporter.java b/org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerServiceExporter.java new file mode 100644 index 00000000000..d73ca8ced37 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/remoting/JmsInvokerServiceExporter.java @@ -0,0 +1,191 @@ +/* + * Copyright 2002-2008 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.remoting; + +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageFormatException; +import javax.jms.MessageProducer; +import javax.jms.Session; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.jms.listener.SessionAwareMessageListener; +import org.springframework.jms.support.JmsUtils; +import org.springframework.jms.support.converter.MessageConverter; +import org.springframework.jms.support.converter.SimpleMessageConverter; +import org.springframework.remoting.support.RemoteInvocation; +import org.springframework.remoting.support.RemoteInvocationBasedExporter; +import org.springframework.remoting.support.RemoteInvocationResult; + +/** + * JMS message listener that exports the specified service bean as a + * JMS service endpoint, accessible via a JMS invoker proxy. + * + *

Note that this class implements Spring's + * {@link org.springframework.jms.listener.SessionAwareMessageListener} + * interface, since it requires access to the active JMS Session. + * Hence, this class can only be used with message listener containers + * which support the SessionAwareMessageListener interface (e.g. Spring's + * {@link org.springframework.jms.listener.DefaultMessageListenerContainer}). + * + *

Thanks to James Strachan for the original prototype that this + * JMS invoker mechanism was inspired by! + * + * @author Juergen Hoeller + * @author James Strachan + * @since 2.0 + * @see JmsInvokerClientInterceptor + * @see JmsInvokerProxyFactoryBean + */ +public class JmsInvokerServiceExporter extends RemoteInvocationBasedExporter + implements SessionAwareMessageListener, InitializingBean { + + private MessageConverter messageConverter = new SimpleMessageConverter(); + + private boolean ignoreInvalidRequests = true; + + private Object proxy; + + + /** + * Specify the MessageConverter to use for turning request messages into + * {@link org.springframework.remoting.support.RemoteInvocation} objects, + * as well as {@link org.springframework.remoting.support.RemoteInvocationResult} + * objects into response messages. + *

Default is a {@link org.springframework.jms.support.converter.SimpleMessageConverter}, + * using a standard JMS {@link javax.jms.ObjectMessage} for each invocation / + * invocation result object. + *

Custom implementations may generally adapt Serializables into + * special kinds of messages, or might be specifically tailored for + * translating RemoteInvocation(Result)s into specific kinds of messages. + */ + public void setMessageConverter(MessageConverter messageConverter) { + this.messageConverter = (messageConverter != null ? messageConverter : new SimpleMessageConverter()); + } + + /** + * Set whether invalidly formatted messages should be discarded. + * Default is "true". + *

Switch this flag to "false" to throw an exception back to the + * listener container. This will typically lead to redelivery of + * the message, which is usually undesirable - since the message + * content will be the same (that is, still invalid). + */ + public void setIgnoreInvalidRequests(boolean ignoreInvalidRequests) { + this.ignoreInvalidRequests = ignoreInvalidRequests; + } + + public void afterPropertiesSet() { + this.proxy = getProxyForService(); + } + + + public void onMessage(Message requestMessage, Session session) throws JMSException { + RemoteInvocation invocation = readRemoteInvocation(requestMessage); + if (invocation != null) { + RemoteInvocationResult result = invokeAndCreateResult(invocation, this.proxy); + writeRemoteInvocationResult(requestMessage, session, result); + } + } + + /** + * Read a RemoteInvocation from the given JMS message. + * @param requestMessage current request message + * @return the RemoteInvocation object (or null + * in case of an invalid message that will simply be ignored) + * @throws javax.jms.JMSException in case of message access failure + */ + protected RemoteInvocation readRemoteInvocation(Message requestMessage) throws JMSException { + Object content = this.messageConverter.fromMessage(requestMessage); + if (content instanceof RemoteInvocation) { + return (RemoteInvocation) content; + } + return onInvalidRequest(requestMessage); + } + + + /** + * Send the given RemoteInvocationResult as a JMS message to the originator. + * @param requestMessage current request message + * @param session the JMS Session to use + * @param result the RemoteInvocationResult object + * @throws javax.jms.JMSException if thrown by trying to send the message + */ + protected void writeRemoteInvocationResult( + Message requestMessage, Session session, RemoteInvocationResult result) throws JMSException { + + Message response = createResponseMessage(requestMessage, session, result); + MessageProducer producer = session.createProducer(requestMessage.getJMSReplyTo()); + try { + producer.send(response); + } + finally { + JmsUtils.closeMessageProducer(producer); + } + } + + /** + * Create the invocation result response message. + *

The default implementation creates a JMS ObjectMessage for the given + * RemoteInvocationResult object. It 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 request message + * @param session the JMS session to use + * @param result the invocation result + * @return the message response to send + * @throws javax.jms.JMSException if creating the messsage failed + */ + protected Message createResponseMessage(Message request, Session session, RemoteInvocationResult result) + throws JMSException { + + Message response = this.messageConverter.toMessage(result, session); + String correlation = request.getJMSCorrelationID(); + if (correlation == null) { + correlation = request.getJMSMessageID(); + } + response.setJMSCorrelationID(correlation); + return response; + } + + /** + * Callback that is invoked by {@link #readRemoteInvocation} + * when it encounters an invalid request message. + *

The default implementation either discards the invalid message or + * throws a MessageFormatException - according to the "ignoreInvalidRequests" + * flag, which is set to "true" (that is, discard invalid messages) by default. + * @param requestMessage the invalid request message + * @return the RemoteInvocation to expose for the invalid request (typically + * null in case of an invalid message that will simply be ignored) + * @throws javax.jms.JMSException in case of the invalid request supposed + * to lead to an exception (instead of ignoring it) + * @see #readRemoteInvocation + * @see #setIgnoreInvalidRequests + */ + protected RemoteInvocation onInvalidRequest(Message requestMessage) throws JMSException { + if (this.ignoreInvalidRequests) { + if (logger.isWarnEnabled()) { + logger.warn("Invalid request message will be discarded: " + requestMessage); + } + return null; + } + else { + throw new MessageFormatException("Invalid request message: " + requestMessage); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/remoting/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/remoting/package.html new file mode 100644 index 00000000000..b5d2f865be3 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/remoting/package.html @@ -0,0 +1,11 @@ + + + +Remoting classes for transparent Java-to-Java remoting via a JMS provider. + +

Allows the target service to be load-balanced across a number of queue +receivers, and provides a level of indirection between the client and the +service: They only need to agree on a queue name and a service interface. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/JmsAccessor.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/JmsAccessor.java new file mode 100644 index 00000000000..1c6ffdc5ac4 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/JmsAccessor.java @@ -0,0 +1,212 @@ +/* + * Copyright 2002-2008 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 javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.Session; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.Constants; +import org.springframework.jms.JmsException; + +/** + * Base class for {@link org.springframework.jms.core.JmsTemplate} and other + * JMS-accessing gateway helpers, defining common properties such as the + * JMS {@link ConnectionFactory} to operate on. The subclass + * {@link org.springframework.jms.support.destination.JmsDestinationAccessor} + * adds further, destination-related properties. + * + *

Not intended to be used directly. + * See {@link org.springframework.jms.core.JmsTemplate}. + * + * @author Juergen Hoeller + * @since 1.2 + * @see org.springframework.jms.support.destination.JmsDestinationAccessor + * @see org.springframework.jms.core.JmsTemplate + */ +public abstract class JmsAccessor implements InitializingBean { + + /** Constants instance for javax.jms.Session */ + private static final Constants sessionConstants = new Constants(Session.class); + + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private ConnectionFactory connectionFactory; + + private boolean sessionTransacted = false; + + private int sessionAcknowledgeMode = Session.AUTO_ACKNOWLEDGE; + + + /** + * Set the ConnectionFactory to use for obtaining JMS {@link Connection Connections}. + */ + public void setConnectionFactory(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + /** + * Return the ConnectionFactory that this accessor uses for obtaining + * JMS {@link Connection Connections}. + */ + public ConnectionFactory getConnectionFactory() { + return this.connectionFactory; + } + + /** + * Set the transaction mode that is used when creating a JMS {@link Session}. + * Default is "false". + *

Note that within a JTA transaction, the parameters passed to + * create(Queue/Topic)Session(boolean transacted, int acknowledgeMode) + * method are not taken into account. Depending on the J2EE transaction context, + * the container makes its own decisions on these values. Analogously, these + * parameters are not taken into account within a locally managed transaction + * either, since the accessor operates on an existing JMS Session in this case. + *

Setting this flag to "true" will use a short local JMS transaction + * when running outside of a managed transaction, and a synchronized local + * JMS transaction in case of a managed transaction (other than an XA + * transaction) being present. The latter has the effect of a local JMS + * transaction being managed alongside the main transaction (which might + * be a native JDBC transaction), with the JMS transaction committing + * right after the main transaction. + * @see javax.jms.Connection#createSession(boolean, int) + */ + public void setSessionTransacted(boolean sessionTransacted) { + this.sessionTransacted = sessionTransacted; + } + + /** + * Return whether the JMS {@link Session sessions} used by this + * accessor are supposed to be transacted. + * @see #setSessionTransacted(boolean) + */ + public boolean isSessionTransacted() { + return this.sessionTransacted; + } + + /** + * Set the JMS acknowledgement mode by the name of the corresponding constant + * in the JMS {@link Session} interface, e.g. "CLIENT_ACKNOWLEDGE". + *

If you want to use vendor-specific extensions to the acknowledgment mode, + * use {@link #setSessionAcknowledgeModeName(String)} instead. + * @param constantName the name of the {@link Session} acknowledge mode constant + * @see javax.jms.Session#AUTO_ACKNOWLEDGE + * @see javax.jms.Session#CLIENT_ACKNOWLEDGE + * @see javax.jms.Session#DUPS_OK_ACKNOWLEDGE + * @see javax.jms.Connection#createSession(boolean, int) + */ + public void setSessionAcknowledgeModeName(String constantName) { + setSessionAcknowledgeMode(sessionConstants.asNumber(constantName).intValue()); + } + + /** + * Set the JMS acknowledgement mode that is used when creating a JMS + * {@link Session} to send a message. + *

Default is {@link Session#AUTO_ACKNOWLEDGE}. + *

Vendor-specific extensions to the acknowledgment mode can be set here as well. + *

Note that that inside an EJB the parameters to + * create(Queue/Topic)Session(boolean transacted, int acknowledgeMode) method + * are not taken into account. Depending on the transaction context in the EJB, + * the container makes its own decisions on these values. See section 17.3.5 + * of the EJB spec. + * @param sessionAcknowledgeMode the acknowledgement mode constant + * @see javax.jms.Session#AUTO_ACKNOWLEDGE + * @see javax.jms.Session#CLIENT_ACKNOWLEDGE + * @see javax.jms.Session#DUPS_OK_ACKNOWLEDGE + * @see javax.jms.Connection#createSession(boolean, int) + */ + public void setSessionAcknowledgeMode(int sessionAcknowledgeMode) { + this.sessionAcknowledgeMode = sessionAcknowledgeMode; + } + + /** + * Return the acknowledgement mode for JMS {@link Session sessions}. + */ + public int getSessionAcknowledgeMode() { + return this.sessionAcknowledgeMode; + } + + public void afterPropertiesSet() { + if (getConnectionFactory() == null) { + throw new IllegalArgumentException("Property 'connectionFactory' is required"); + } + } + + + /** + * Convert the specified checked {@link javax.jms.JMSException JMSException} to + * a Spring runtime {@link org.springframework.jms.JmsException JmsException} + * equivalent. + *

The default implementation delegates to the + * {@link org.springframework.jms.support.JmsUtils#convertJmsAccessException} method. + * @param ex the original checked {@link JMSException} to convert + * @return the Spring runtime {@link JmsException} wrapping ex + * @see org.springframework.jms.support.JmsUtils#convertJmsAccessException + */ + protected JmsException convertJmsAccessException(JMSException ex) { + return JmsUtils.convertJmsAccessException(ex); + } + + + //------------------------------------------------------------------------- + // JMS 1.1 factory methods, potentially overridden for JMS 1.0.2 + //------------------------------------------------------------------------- + + /** + * Create a JMS Connection via this template's ConnectionFactory. + *

This implementation uses JMS 1.1 API. + * @return the new JMS Connection + * @throws JMSException if thrown by JMS API methods + * @see javax.jms.ConnectionFactory#createConnection() + */ + protected Connection createConnection() throws JMSException { + return getConnectionFactory().createConnection(); + } + + /** + * Create a JMS Session for the given Connection. + *

This implementation uses JMS 1.1 API. + * @param con the JMS Connection to create a Session for + * @return the new JMS Session + * @throws JMSException if thrown by JMS API methods + * @see javax.jms.Connection#createSession(boolean, int) + */ + protected Session createSession(Connection con) throws JMSException { + return con.createSession(isSessionTransacted(), getSessionAcknowledgeMode()); + } + + /** + * Determine whether the given Session is in client acknowledge mode. + *

This implementation uses JMS 1.1 API. + * @param session the JMS Session to check + * @return whether the given Session is in client acknowledge mode + * @throws javax.jms.JMSException if thrown by JMS API methods + * @see javax.jms.Session#getAcknowledgeMode() + * @see javax.jms.Session#CLIENT_ACKNOWLEDGE + */ + protected boolean isClientAcknowledge(Session session) throws JMSException { + return (session.getAcknowledgeMode() == Session.CLIENT_ACKNOWLEDGE); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/JmsUtils.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/JmsUtils.java new file mode 100644 index 00000000000..0f526b6aca7 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/JmsUtils.java @@ -0,0 +1,311 @@ +/* + * Copyright 2002-2008 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 javax.jms.Connection; +import javax.jms.JMSException; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.QueueBrowser; +import javax.jms.QueueRequestor; +import javax.jms.Session; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.jms.InvalidClientIDException; +import org.springframework.jms.InvalidDestinationException; +import org.springframework.jms.InvalidSelectorException; +import org.springframework.jms.JmsException; +import org.springframework.jms.JmsSecurityException; +import org.springframework.jms.MessageEOFException; +import org.springframework.jms.MessageFormatException; +import org.springframework.jms.MessageNotReadableException; +import org.springframework.jms.MessageNotWriteableException; +import org.springframework.jms.ResourceAllocationException; +import org.springframework.jms.TransactionInProgressException; +import org.springframework.jms.TransactionRolledBackException; +import org.springframework.jms.UncategorizedJmsException; +import org.springframework.util.Assert; + +/** + * Generic utility methods for working with JMS. Mainly for internal use + * within the framework, but also useful for custom JMS access code. + * + * @author Juergen Hoeller + * @since 1.1 + */ +public abstract class JmsUtils { + + private static final Log logger = LogFactory.getLog(JmsUtils.class); + + + /** + * Close the given JMS Connection and ignore any thrown exception. + * This is useful for typical finally blocks in manual JMS code. + * @param con the JMS Connection to close (may be null) + */ + public static void closeConnection(Connection con) { + closeConnection(con, false); + } + + /** + * Close the given JMS Connection and ignore any thrown exception. + * This is useful for typical finally blocks in manual JMS code. + * @param con the JMS Connection to close (may be null) + * @param stop whether to call stop() before closing + */ + public static void closeConnection(Connection con, boolean stop) { + if (con != null) { + try { + if (stop) { + try { + con.stop(); + } + finally { + con.close(); + } + } + else { + con.close(); + } + } + catch (javax.jms.IllegalStateException ex) { + logger.debug("Ignoring Connection state exception - assuming already closed: " + ex); + } + catch (JMSException ex) { + logger.debug("Could not close JMS Connection", ex); + } + catch (Throwable ex) { + // We don't trust the JMS provider: It might throw RuntimeException or Error. + logger.debug("Unexpected exception on closing JMS Connection", ex); + } + } + } + + /** + * Close the given JMS Session and ignore any thrown exception. + * This is useful for typical finally blocks in manual JMS code. + * @param session the JMS Session to close (may be null) + */ + public static void closeSession(Session session) { + if (session != null) { + try { + session.close(); + } + catch (JMSException ex) { + logger.trace("Could not close JMS Session", ex); + } + catch (Throwable ex) { + // We don't trust the JMS provider: It might throw RuntimeException or Error. + logger.trace("Unexpected exception on closing JMS Session", ex); + } + } + } + + /** + * Close the given JMS MessageProducer and ignore any thrown exception. + * This is useful for typical finally blocks in manual JMS code. + * @param producer the JMS MessageProducer to close (may be null) + */ + public static void closeMessageProducer(MessageProducer producer) { + if (producer != null) { + try { + producer.close(); + } + catch (JMSException ex) { + logger.trace("Could not close JMS MessageProducer", ex); + } + catch (Throwable ex) { + // We don't trust the JMS provider: It might throw RuntimeException or Error. + logger.trace("Unexpected exception on closing JMS MessageProducer", ex); + } + } + } + + /** + * Close the given JMS MessageConsumer and ignore any thrown exception. + * This is useful for typical finally blocks in manual JMS code. + * @param consumer the JMS MessageConsumer to close (may be null) + */ + public static void closeMessageConsumer(MessageConsumer consumer) { + if (consumer != null) { + // Clear interruptions to ensure that the consumer closes successfully... + // (working around misbehaving JMS providers such as ActiveMQ) + boolean wasInterrupted = Thread.interrupted(); + try { + consumer.close(); + } + catch (JMSException ex) { + logger.trace("Could not close JMS MessageConsumer", ex); + } + catch (Throwable ex) { + // We don't trust the JMS provider: It might throw RuntimeException or Error. + logger.trace("Unexpected exception on closing JMS MessageConsumer", ex); + } + finally { + if (wasInterrupted) { + // Reset the interrupted flag as it was before. + Thread.currentThread().interrupt(); + } + } + } + } + + /** + * Close the given JMS QueueBrowser and ignore any thrown exception. + * This is useful for typical finally blocks in manual JMS code. + * @param browser the JMS QueueBrowser to close (may be null) + */ + public static void closeQueueBrowser(QueueBrowser browser) { + if (browser != null) { + try { + browser.close(); + } + catch (JMSException ex) { + logger.trace("Could not close JMS QueueBrowser", ex); + } + catch (Throwable ex) { + // We don't trust the JMS provider: It might throw RuntimeException or Error. + logger.trace("Unexpected exception on closing JMS QueueBrowser", ex); + } + } + } + + /** + * Close the given JMS QueueRequestor and ignore any thrown exception. + * This is useful for typical finally blocks in manual JMS code. + * @param requestor the JMS QueueRequestor to close (may be null) + */ + public static void closeQueueRequestor(QueueRequestor requestor) { + if (requestor != null) { + try { + requestor.close(); + } + catch (JMSException ex) { + logger.trace("Could not close JMS QueueRequestor", ex); + } + catch (Throwable ex) { + // We don't trust the JMS provider: It might throw RuntimeException or Error. + logger.trace("Unexpected exception on closing JMS QueueRequestor", ex); + } + } + } + + /** + * Commit the Session if not within a JTA transaction. + * @param session the JMS Session to commit + * @throws JMSException if committing failed + */ + public static void commitIfNecessary(Session session) throws JMSException { + Assert.notNull(session, "Session must not be null"); + try { + session.commit(); + } + catch (javax.jms.TransactionInProgressException ex) { + // Ignore -> can only happen in case of a JTA transaction. + } + catch (javax.jms.IllegalStateException ex) { + // Ignore -> can only happen in case of a JTA transaction. + } + } + + /** + * Rollback the Session if not within a JTA transaction. + * @param session the JMS Session to rollback + * @throws JMSException if committing failed + */ + public static void rollbackIfNecessary(Session session) throws JMSException { + Assert.notNull(session, "Session must not be null"); + try { + session.rollback(); + } + catch (javax.jms.TransactionInProgressException ex) { + // Ignore -> can only happen in case of a JTA transaction. + } + catch (javax.jms.IllegalStateException ex) { + // Ignore -> can only happen in case of a JTA transaction. + } + } + + /** + * Build a descriptive exception message for the given JMSException, + * incorporating a linked exception's message if appropriate. + * @param ex the JMSException to build a message for + * @return the descriptive message String + * @see javax.jms.JMSException#getLinkedException() + */ + public static String buildExceptionMessage(JMSException ex) { + String message = ex.getMessage(); + Exception linkedEx = ex.getLinkedException(); + if (linkedEx != null && message.indexOf(linkedEx.getMessage()) == -1) { + message = message + "; nested exception is " + linkedEx; + } + return message; + } + + /** + * Convert the specified checked {@link javax.jms.JMSException JMSException} to a + * Spring runtime {@link org.springframework.jms.JmsException JmsException} equivalent. + * @param ex the original checked JMSException to convert + * @return the Spring runtime JmsException wrapping the given exception + */ + public static JmsException convertJmsAccessException(JMSException ex) { + Assert.notNull(ex, "JMSException must not be null"); + + if (ex instanceof javax.jms.IllegalStateException) { + return new org.springframework.jms.IllegalStateException((javax.jms.IllegalStateException) ex); + } + if (ex instanceof javax.jms.InvalidClientIDException) { + return new InvalidClientIDException((javax.jms.InvalidClientIDException) ex); + } + if (ex instanceof javax.jms.InvalidDestinationException) { + return new InvalidDestinationException((javax.jms.InvalidDestinationException) ex); + } + if (ex instanceof javax.jms.InvalidSelectorException) { + return new InvalidSelectorException((javax.jms.InvalidSelectorException) ex); + } + if (ex instanceof javax.jms.JMSSecurityException) { + return new JmsSecurityException((javax.jms.JMSSecurityException) ex); + } + if (ex instanceof javax.jms.MessageEOFException) { + return new MessageEOFException((javax.jms.MessageEOFException) ex); + } + if (ex instanceof javax.jms.MessageFormatException) { + return new MessageFormatException((javax.jms.MessageFormatException) ex); + } + if (ex instanceof javax.jms.MessageNotReadableException) { + return new MessageNotReadableException((javax.jms.MessageNotReadableException) ex); + } + if (ex instanceof javax.jms.MessageNotWriteableException) { + return new MessageNotWriteableException((javax.jms.MessageNotWriteableException) ex); + } + if (ex instanceof javax.jms.ResourceAllocationException) { + return new ResourceAllocationException((javax.jms.ResourceAllocationException) ex); + } + if (ex instanceof javax.jms.TransactionInProgressException) { + return new TransactionInProgressException((javax.jms.TransactionInProgressException) ex); + } + if (ex instanceof javax.jms.TransactionRolledBackException) { + return new TransactionRolledBackException((javax.jms.TransactionRolledBackException) ex); + } + + // fallback + return new UncategorizedJmsException(ex); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MessageConversionException.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MessageConversionException.java new file mode 100644 index 00000000000..29af514957b --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MessageConversionException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2006 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 org.springframework.jms.JmsException; + +/** + * Thrown by {@link MessageConverter} implementations when the conversion + * of an object to/from a {@link javax.jms.Message} fails. + * + * @author Mark Pollack + * @since 1.1 + * @see MessageConverter + */ +public class MessageConversionException extends JmsException { + + /** + * Create a new MessageConversionException. + * @param msg the detail message + */ + public MessageConversionException(String msg) { + super(msg); + } + + /** + * Create a new MessageConversionException. + * @param msg the detail message + * @param cause the root cause (if any) + */ + public MessageConversionException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MessageConverter.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MessageConverter.java new file mode 100644 index 00000000000..b4661008301 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/MessageConverter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2007 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.JMSException; +import javax.jms.Message; +import javax.jms.Session; + +/** + * Strategy interface that specifies a converter between Java objects and JMS messages. + * + *

Check out {@link SimpleMessageConverter} for a default implementation, + * converting between the 'standard' message payloads and JMS Message types. + * + * @author Mark Pollack + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.jms.core.JmsTemplate#setMessageConverter + * @see org.springframework.jms.listener.adapter.MessageListenerAdapter#setMessageConverter + * @see org.springframework.jms.remoting.JmsInvokerClientInterceptor#setMessageConverter + * @see org.springframework.jms.remoting.JmsInvokerServiceExporter#setMessageConverter + */ +public interface MessageConverter { + + /** + * Convert a Java object to a JMS Message using the supplied session + * to create the message object. + * @param object the object to convert + * @param session the Session to use for creating a JMS Message + * @return the JMS Message + * @throws javax.jms.JMSException if thrown by JMS API methods + * @throws MessageConversionException in case of conversion failure + */ + Message toMessage(Object object, Session session) throws JMSException, MessageConversionException; + + /** + * Convert from a JMS Message to a Java object. + * @param message the message to convert + * @return the converted Java object + * @throws javax.jms.JMSException if thrown by JMS API methods + * @throws MessageConversionException in case of conversion failure + */ + Object fromMessage(Message message) throws JMSException, MessageConversionException; + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter.java new file mode 100644 index 00000000000..72363ec487c --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter.java @@ -0,0 +1,227 @@ +/* + * Copyright 2002-2008 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.io.Serializable; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.jms.BytesMessage; +import javax.jms.JMSException; +import javax.jms.MapMessage; +import javax.jms.Message; +import javax.jms.ObjectMessage; +import javax.jms.Session; +import javax.jms.TextMessage; + +import org.springframework.util.ObjectUtils; + +/** + * A simple message converter which is able to handle TextMessages, BytesMessages, + * MapMessages, and ObjectMessages. Used as default conversion strategy + * by {@link org.springframework.jms.core.JmsTemplate}, for + * convertAndSend and receiveAndConvert operations. + * + *

Converts a String to a {@link javax.jms.TextMessage}, a byte array to a + * {@link javax.jms.BytesMessage}, a Map to a {@link javax.jms.MapMessage}, and + * a Serializable object to a {@link javax.jms.ObjectMessage} (or vice versa). + * + *

This converter implementation works for both JMS 1.1 and JMS 1.0.2, + * except when extracting a byte array from a BytesMessage. So for converting + * BytesMessages with a JMS 1.0.2 provider, use {@link SimpleMessageConverter102}. + * (As you would expect, {@link org.springframework.jms.core.JmsTemplate102} + * uses SimpleMessageConverter102 as default.) + * + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.jms.core.JmsTemplate#convertAndSend + * @see org.springframework.jms.core.JmsTemplate#receiveAndConvert + */ +public class SimpleMessageConverter implements MessageConverter { + + /** + * This implementation creates a TextMessage for a String, a + * BytesMessage for a byte array, a MapMessage for a Map, + * and an ObjectMessage for a Serializable object. + * @see #createMessageForString + * @see #createMessageForByteArray + * @see #createMessageForMap + * @see #createMessageForSerializable + */ + public Message toMessage(Object object, Session session) throws JMSException, MessageConversionException { + if (object instanceof Message) { + return (Message) object; + } + else if (object instanceof String) { + return createMessageForString((String) object, session); + } + else if (object instanceof byte[]) { + return createMessageForByteArray((byte[]) object, session); + } + else if (object instanceof Map) { + return createMessageForMap((Map) object, session); + } + else if (object instanceof Serializable) { + return createMessageForSerializable(((Serializable) object), session); + } + else { + throw new MessageConversionException("Cannot convert object of type [" + + ObjectUtils.nullSafeClassName(object) + "] to JMS message. Supported message " + + "payloads are: String, byte array, Map, Serializable object."); + } + } + + /** + * This implementation converts a TextMessage back to a String, a + * ByteMessage back to a byte array, a MapMessage back to a Map, + * and an ObjectMessage back to a Serializable object. Returns + * the plain Message object in case of an unknown message type. + * @see #extractStringFromMessage + * @see #extractByteArrayFromMessage + * @see #extractMapFromMessage + * @see #extractSerializableFromMessage + */ + public Object fromMessage(Message message) throws JMSException, MessageConversionException { + if (message instanceof TextMessage) { + return extractStringFromMessage((TextMessage) message); + } + else if (message instanceof BytesMessage) { + return extractByteArrayFromMessage((BytesMessage) message); + } + else if (message instanceof MapMessage) { + return extractMapFromMessage((MapMessage) message); + } + else if (message instanceof ObjectMessage) { + return extractSerializableFromMessage((ObjectMessage) message); + } + else { + return message; + } + } + + + /** + * Create a JMS TextMessage for the given String. + * @param text the String to convert + * @param session current JMS session + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @see javax.jms.Session#createTextMessage + */ + protected TextMessage createMessageForString(String text, Session session) throws JMSException { + return session.createTextMessage(text); + } + + /** + * Create a JMS BytesMessage for the given byte array. + * @param bytes the byyte array to convert + * @param session current JMS session + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @see javax.jms.Session#createBytesMessage + */ + protected BytesMessage createMessageForByteArray(byte[] bytes, Session session) throws JMSException { + BytesMessage message = session.createBytesMessage(); + message.writeBytes(bytes); + return message; + } + + /** + * Create a JMS MapMessage for the given Map. + * @param map the Map to convert + * @param session current JMS session + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @see javax.jms.Session#createMapMessage + */ + protected MapMessage createMessageForMap(Map map, Session session) throws JMSException { + MapMessage message = session.createMapMessage(); + for (Iterator it = map.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + if (!(entry.getKey() instanceof String)) { + throw new MessageConversionException("Cannot convert non-String key of type [" + + ObjectUtils.nullSafeClassName(entry.getKey()) + "] to JMS MapMessage entry"); + } + message.setObject((String) entry.getKey(), entry.getValue()); + } + return message; + } + + /** + * Create a JMS ObjectMessage for the given Serializable object. + * @param object the Serializable object to convert + * @param session current JMS session + * @return the resulting message + * @throws JMSException if thrown by JMS methods + * @see javax.jms.Session#createObjectMessage + */ + protected ObjectMessage createMessageForSerializable(Serializable object, Session session) throws JMSException { + return session.createObjectMessage(object); + } + + + /** + * Extract a String from the given TextMessage. + * @param message the message to convert + * @return the resulting String + * @throws JMSException if thrown by JMS methods + */ + protected String extractStringFromMessage(TextMessage message) throws JMSException { + return message.getText(); + } + + /** + * Extract a byte array from the given {@link BytesMessage}. + * @param message the message to convert + * @return the resulting byte array + * @throws JMSException if thrown by JMS methods + */ + protected byte[] extractByteArrayFromMessage(BytesMessage message) throws JMSException { + byte[] bytes = new byte[(int) message.getBodyLength()]; + message.readBytes(bytes); + return bytes; + } + + /** + * Extract a Map from the given {@link MapMessage}. + * @param message the message to convert + * @return the resulting Map + * @throws JMSException if thrown by JMS methods + */ + protected Map extractMapFromMessage(MapMessage message) throws JMSException { + Map map = new HashMap(); + Enumeration en = message.getMapNames(); + while (en.hasMoreElements()) { + String key = (String) en.nextElement(); + map.put(key, message.getObject(key)); + } + return map; + } + + /** + * Extract a Serializable object from the given {@link ObjectMessage}. + * @param message the message to convert + * @return the resulting Serializable object + * @throws JMSException if thrown by JMS methods + */ + protected Serializable extractSerializableFromMessage(ObjectMessage message) throws JMSException { + return message.getObject(); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter102.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter102.java new file mode 100644 index 00000000000..8272917c8a5 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/SimpleMessageConverter102.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2007 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.io.ByteArrayOutputStream; + +import javax.jms.BytesMessage; +import javax.jms.JMSException; + +/** + * A subclass of {@link SimpleMessageConverter} for the JMS 1.0.2 specification, + * not relying on JMS 1.1 methods like SimpleMessageConverter itself. + * This class can be used for JMS 1.0.2 providers, offering the same functionality + * as SimpleMessageConverter does for JMS 1.1 providers. + * + *

The only difference to the default SimpleMessageConverter is that BytesMessage + * is handled differently: namely, without using the getBodyLength() + * method which has been introduced in JMS 1.1 and is therefore not available on a + * JMS 1.0.2 provider. + * + * @author Juergen Hoeller + * @since 1.1.1 + * @see javax.jms.BytesMessage#getBodyLength() + */ +public class SimpleMessageConverter102 extends SimpleMessageConverter { + + public static final int BUFFER_SIZE = 4096; + + + /** + * Overrides superclass method to copy bytes from the message into a + * ByteArrayOutputStream, using a buffer, to avoid using the + * getBodyLength() method which has been introduced in + * JMS 1.1 and is therefore not available on a JMS 1.0.2 provider. + * @see javax.jms.BytesMessage#getBodyLength() + */ + protected byte[] extractByteArrayFromMessage(BytesMessage message) throws JMSException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(BUFFER_SIZE); + byte[] buffer = new byte[BUFFER_SIZE]; + int bufferCount = -1; + while ((bufferCount = message.readBytes(buffer)) >= 0) { + baos.write(buffer, 0, bufferCount); + if (bufferCount < BUFFER_SIZE) { + break; + } + } + return baos.toByteArray(); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/package.html new file mode 100644 index 00000000000..bde2953ca69 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/converter/package.html @@ -0,0 +1,8 @@ + + + +Provides a MessageConverter abstraction to convert +between Java objects and JMS messages. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/BeanFactoryDestinationResolver.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/BeanFactoryDestinationResolver.java new file mode 100644 index 00000000000..d1f6ddd3f38 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/BeanFactoryDestinationResolver.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2007 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.destination; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Session; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.util.Assert; + +/** + * {@link DestinationResolver} implementation based on a Spring {@link BeanFactory}. + * + *

Will lookup Spring managed beans identified by bean name, + * expecting them to be of type javax.jms.Destination. + * + * @author Juergen Hoeller + * @since 2.5 + * @see org.springframework.beans.factory.BeanFactory + */ +public class BeanFactoryDestinationResolver implements DestinationResolver, BeanFactoryAware { + + private BeanFactory beanFactory; + + + /** + * Create a new instance of the {@link BeanFactoryDestinationResolver} class. + *

The BeanFactory to access must be set via setBeanFactory. + * @see #setBeanFactory + */ + public BeanFactoryDestinationResolver() { + } + + /** + * Create a new instance of the {@link BeanFactoryDestinationResolver} class. + *

Use of this constructor is redundant if this object is being created + * by a Spring IoC container, as the supplied {@link BeanFactory} will be + * replaced by the {@link BeanFactory} that creates it (c.f. the + * {@link BeanFactoryAware} contract). So only use this constructor if you + * are using this class outside the context of a Spring IoC container. + * @param beanFactory the bean factory to be used to lookup {@link javax.jms.Destination Destinatiosn} + */ + public BeanFactoryDestinationResolver(BeanFactory beanFactory) { + Assert.notNull(beanFactory, "BeanFactory is required"); + this.beanFactory = beanFactory; + } + + + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + + public Destination resolveDestinationName(Session session, String destinationName, boolean pubSubDomain) + throws JMSException { + + Assert.state(this.beanFactory != null, "BeanFactory is required"); + try { + return (Destination) this.beanFactory.getBean(destinationName, Destination.class); + } + catch (BeansException ex) { + throw new DestinationResolutionException( + "Failed to look up Destinaton bean with name '" + destinationName + "'", ex); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/CachingDestinationResolver.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/CachingDestinationResolver.java new file mode 100644 index 00000000000..308181d9a03 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/CachingDestinationResolver.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2006 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.destination; + +/** + * Extension of the DestinationResolver interface, + * exposing methods for clearing the cache. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public interface CachingDestinationResolver extends DestinationResolver { + + /** + * Remove the destination with the given name from the cache + * (if cached by this resolver in the first place). + *

To be called if access to the specified destination failed, + * assuming that the JMS Destination object might have become invalid. + * @param destinationName the name of the destination + */ + void removeFromCache(String destinationName); + + /** + * Clear the entire destination cache. + *

To be called in case of general JMS provider failure. + */ + void clearCache(); + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DestinationResolutionException.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DestinationResolutionException.java new file mode 100644 index 00000000000..3372151333e --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DestinationResolutionException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2006 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.destination; + +import org.springframework.jms.JmsException; + +/** + * Thrown by a DestinationResolver when it cannot resolve a destination name. + * + * @author Juergen Hoeller + * @since 1.1 + * @see DestinationResolver + */ +public class DestinationResolutionException extends JmsException { + + /** + * Create a new DestinationResolutionException. + * @param msg the detail message + */ + public DestinationResolutionException(String msg) { + super(msg); + } + + /** + * Create a new DestinationResolutionException. + * @param msg the detail message + * @param cause the root cause (if any) + */ + public DestinationResolutionException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java new file mode 100644 index 00000000000..7f5d5d6ca4b --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2007 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.destination; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Session; + +/** + * Strategy interface for resolving JMS destinations. + * + *

Used by {@link org.springframework.jms.core.JmsTemplate} for resolving + * destination names from simple {@link String Strings} to actual + * {@link Destination} implementation instances. + * + *

The default {@link DestinationResolver} implementation used by + * {@link org.springframework.jms.core.JmsTemplate} instances is the + * {@link DynamicDestinationResolver} class. Consider using the + * {@link JndiDestinationResolver} for more advanced scenarios. + * + * @author Juergen Hoeller + * @since 1.1 + * @see org.springframework.jms.core.JmsTemplate#setDestinationResolver + * @see org.springframework.jms.support.destination.DynamicDestinationResolver + * @see org.springframework.jms.support.destination.JndiDestinationResolver + */ +public interface DestinationResolver { + + /** + * Resolve the given destination name, either as located resource + * or as dynamic destination. + * @param session the current JMS Session + * (may be null if the resolver implementation is able to work without it) + * @param destinationName the name of the destination + * @param pubSubDomain true if the domain is pub-sub, false if P2P + * @return the JMS destination (either a topic or a queue) + * @throws javax.jms.JMSException if the JMS Session failed to resolve the destination + * @throws DestinationResolutionException in case of general destination resolution failure + */ + Destination resolveDestinationName(Session session, String destinationName, boolean pubSubDomain) + throws JMSException; + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java new file mode 100644 index 00000000000..4bbf8fa1001 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2006 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.destination; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Queue; +import javax.jms.QueueSession; +import javax.jms.Session; +import javax.jms.Topic; +import javax.jms.TopicSession; + +import org.springframework.util.Assert; + +/** + * Simple {@link DestinationResolver} implementation resolving destination names + * as dynamic destinations. + * + *

This implementation will work on both JMS 1.1 and JMS 1.0.2, + * because it uses the {@link javax.jms.QueueSession} or {@link javax.jms.TopicSession} + * methods if possible, falling back to JMS 1.1's generic {@link javax.jms.Session} + * methods. + * + * @author Juergen Hoeller + * @since 1.1 + * @see javax.jms.QueueSession#createQueue + * @see javax.jms.TopicSession#createTopic + * @see javax.jms.Session#createQueue + * @see javax.jms.Session#createTopic + */ +public class DynamicDestinationResolver implements DestinationResolver { + + /** + * Resolve the specified destination name as a dynamic destination. + * @param session the current JMS Session + * @param destinationName the name of the destination + * @param pubSubDomain true if the domain is pub-sub, false if P2P + * @return the JMS destination (either a topic or a queue) + * @throws javax.jms.JMSException if resolution failed + * @see #resolveTopic(javax.jms.Session, String) + * @see #resolveQueue(javax.jms.Session, String) + */ + public Destination resolveDestinationName(Session session, String destinationName, boolean pubSubDomain) + throws JMSException { + + Assert.notNull(session, "Session must not be null"); + Assert.notNull(destinationName, "Destination name must not be null"); + if (pubSubDomain) { + return resolveTopic(session, destinationName); + } + else { + return resolveQueue(session, destinationName); + } + } + + + /** + * Resolve the given destination name to a {@link Topic}. + * @param session the current JMS Session + * @param topicName the name of the desired {@link Topic} + * @return the JMS {@link Topic} + * @throws javax.jms.JMSException if resolution failed + * @see Session#createTopic(String) + */ + protected Topic resolveTopic(Session session, String topicName) throws JMSException { + if (session instanceof TopicSession) { + // Cast to TopicSession: will work on both JMS 1.1 and 1.0.2 + return ((TopicSession) session).createTopic(topicName); + } + else { + // Fall back to generic JMS Session: will only work on JMS 1.1 + return session.createTopic(topicName); + } + } + + /** + * Resolve the given destination name to a {@link Queue}. + * @param session the current JMS Session + * @param queueName the name of the desired {@link Queue} + * @return the JMS {@link Queue} + * @throws javax.jms.JMSException if resolution failed + * @see Session#createQueue(String) + */ + protected Queue resolveQueue(Session session, String queueName) throws JMSException { + if (session instanceof QueueSession) { + // Cast to QueueSession: will work on both JMS 1.1 and 1.0.2 + return ((QueueSession) session).createQueue(queueName); + } + else { + // Fall back to generic JMS Session: will only work on JMS 1.1 + return session.createQueue(queueName); + } + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java new file mode 100644 index 00000000000..335e14800eb --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java @@ -0,0 +1,103 @@ +/* + * Copyright 2002-2007 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.destination; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Session; + +import org.springframework.jms.support.JmsAccessor; +import org.springframework.util.Assert; + +/** + * Base class for {@link org.springframework.jms.core.JmsTemplate} and other + * JMS-accessing gateway helpers, adding destination-related properties to + * {@link JmsAccessor JmsAccessor's} common properties. + * + *

Not intended to be used directly. + * See {@link org.springframework.jms.core.JmsTemplate}. + * + * @author Juergen Hoeller + * @since 1.2.5 + * @see org.springframework.jms.support.JmsAccessor + * @see org.springframework.jms.core.JmsTemplate + */ +public abstract class JmsDestinationAccessor extends JmsAccessor { + + private DestinationResolver destinationResolver = new DynamicDestinationResolver(); + + private boolean pubSubDomain = false; + + + /** + * Set the {@link DestinationResolver} that is to be used to resolve + * {@link javax.jms.Destination} references for this accessor. + *

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 accessor (never null). + */ + public DestinationResolver getDestinationResolver() { + return this.destinationResolver; + } + + /** + * Configure the destination accessor with knowledge of the JMS domain used. + * Default is Point-to-Point (Queues). + *

For JMS 1.0.2 based accessors, this tells the JMS provider which class hierarchy + * to use in the implementation of its operations. For JMS 1.1 based accessors, this + * setting does usually not affect operations. However, for both JMS versions, this + * setting tells what type of destination to resolve if dynamic destinations are enabled. + * @param pubSubDomain "true" for the Publish/Subscribe domain ({@link javax.jms.Topic Topics}), + * "false" for the Point-to-Point domain ({@link javax.jms.Queue Queues}) + * @see #setDestinationResolver + */ + public void setPubSubDomain(boolean pubSubDomain) { + this.pubSubDomain = pubSubDomain; + } + + /** + * Return whether the Publish/Subscribe domain ({@link javax.jms.Topic Topics}) is used. + * Otherwise, the Point-to-Point domain ({@link javax.jms.Queue Queues}) is used. + */ + public boolean isPubSubDomain() { + return this.pubSubDomain; + } + + + /** + * Resolve the given destination name into a JMS {@link Destination}, + * via this accessor's {@link DestinationResolver}. + * @param session the current JMS {@link Session} + * @param destinationName the name of the destination + * @return the located {@link Destination} + * @throws javax.jms.JMSException if resolution failed + * @see #setDestinationResolver + */ + protected Destination resolveDestinationName(Session session, String destinationName) throws JMSException { + return getDestinationResolver().resolveDestinationName(session, destinationName, isPubSubDomain()); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/JndiDestinationResolver.java b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/JndiDestinationResolver.java new file mode 100644 index 00000000000..0696c596d11 --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/JndiDestinationResolver.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2007 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.destination; + +import java.util.Map; + +import javax.jms.Destination; +import javax.jms.JMSException; +import javax.jms.Queue; +import javax.jms.Session; +import javax.jms.Topic; +import javax.naming.NamingException; + +import org.springframework.core.CollectionFactory; +import org.springframework.jndi.JndiLocatorSupport; +import org.springframework.util.Assert; + +/** + * {@link DestinationResolver} implementation which interprets destination names + * as JNDI locations (with a configurable fallback strategy). + * + *

Allows for customizing the JNDI environment if necessary, for example + * specifying appropriate JNDI environment properties. + * + *

Dynamic queues and topics get cached by destination name. As a consequence, + * you need to use unique destination names across both queues and topics. + * Caching can be turned off through the {@link #setCache "cache"} flag. + * + *

Note that the fallback to resolution of dynamic destinations + * is turned off by default. Switch the + * {@link #setFallbackToDynamicDestination "fallbackToDynamicDestination"} + * flag on to enable this functionality. + * + * @author Mark Pollack + * @author Juergen Hoeller + * @since 1.1 + * @see #setJndiTemplate + * @see #setJndiEnvironment + * @see #setCache + * @see #setFallbackToDynamicDestination + */ +public class JndiDestinationResolver extends JndiLocatorSupport implements CachingDestinationResolver { + + private boolean cache = true; + + private boolean fallbackToDynamicDestination = false; + + private DestinationResolver dynamicDestinationResolver = new DynamicDestinationResolver(); + + private final Map destinationCache = CollectionFactory.createConcurrentMapIfPossible(16); + + + /** + * Set whether to cache resolved destinations. Default is "true". + *

This flag can be turned off to re-lookup a destination for each operation, + * which allows for hot restarting of destinations. This is mainly useful + * during development. + *

Note that dynamic queues and topics get cached by destination name. + * As a consequence, you need to use unique destination names across both + * queues and topics. + */ + public void setCache(boolean cache) { + this.cache = cache; + } + + /** + * Set whether this resolver is supposed to create dynamic destinations + * if the destination name is not found in JNDI. Default is "false". + *

Turn this flag on to enable transparent fallback to dynamic destinations. + * @see #setDynamicDestinationResolver + */ + public void setFallbackToDynamicDestination(boolean fallbackToDynamicDestination) { + this.fallbackToDynamicDestination = fallbackToDynamicDestination; + } + + /** + * Set the {@link DestinationResolver} to use when falling back to dynamic + * destinations. + *

The default is Spring's standard {@link DynamicDestinationResolver}. + * @see #setFallbackToDynamicDestination + * @see DynamicDestinationResolver + */ + public void setDynamicDestinationResolver(DestinationResolver dynamicDestinationResolver) { + this.dynamicDestinationResolver = dynamicDestinationResolver; + } + + + public Destination resolveDestinationName(Session session, String destinationName, boolean pubSubDomain) + throws JMSException { + + Assert.notNull(destinationName, "Destination name must not be null"); + Destination dest = (Destination) this.destinationCache.get(destinationName); + if (dest != null) { + validateDestination(dest, destinationName, pubSubDomain); + } + else { + try { + dest = (Destination) lookup(destinationName, Destination.class); + validateDestination(dest, destinationName, pubSubDomain); + } + catch (NamingException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Destination [" + destinationName + "] not found in JNDI", ex); + } + if (this.fallbackToDynamicDestination) { + dest = this.dynamicDestinationResolver.resolveDestinationName(session, destinationName, pubSubDomain); + } + else { + throw new DestinationResolutionException( + "Destination [" + destinationName + "] not found in JNDI", ex); + } + } + if (this.cache) { + this.destinationCache.put(destinationName, dest); + } + } + return dest; + } + + /** + * Validate the given Destination object, checking whether it matches + * the expected type. + * @param destination the Destination object to validate + * @param destinationName the name of the destination + * @param pubSubDomain true if a Topic is expected, + * false in case of a Queue + */ + protected void validateDestination(Destination destination, String destinationName, boolean pubSubDomain) { + Class targetClass = Queue.class; + if (pubSubDomain) { + targetClass = Topic.class; + } + if (!targetClass.isInstance(destination)) { + throw new DestinationResolutionException( + "Destination [" + destinationName + "] is not of expected type [" + targetClass.getName() + "]"); + } + } + + + public void removeFromCache(String destinationName) { + this.destinationCache.remove(destinationName); + } + + public void clearCache() { + this.destinationCache.clear(); + } + +} diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/package.html new file mode 100644 index 00000000000..4f8e0d50eaf --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/destination/package.html @@ -0,0 +1,7 @@ + + + +Support classes for Spring's JMS framework. + + + diff --git a/org.springframework.jms/src/main/java/org/springframework/jms/support/package.html b/org.springframework.jms/src/main/java/org/springframework/jms/support/package.html new file mode 100644 index 00000000000..3fddd2d8c8d --- /dev/null +++ b/org.springframework.jms/src/main/java/org/springframework/jms/support/package.html @@ -0,0 +1,8 @@ + + + +This package provides generic JMS support classes, +to be used by higher-level classes like JmsTemplate. + + + diff --git a/org.springframework.jms/src/main/java/overview.html b/org.springframework.jms/src/main/java/overview.html new file mode 100644 index 00000000000..1eb7a2e8c19 --- /dev/null +++ b/org.springframework.jms/src/main/java/overview.html @@ -0,0 +1,7 @@ + + +

+The Spring Data Binding framework, an internal library used by Spring Web Flow. +

+ + \ No newline at end of file diff --git a/org.springframework.jms/src/test/resources/log4j.xml b/org.springframework.jms/src/test/resources/log4j.xml new file mode 100644 index 00000000000..767b96d6206 --- /dev/null +++ b/org.springframework.jms/src/test/resources/log4j.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.springframework.jms/template.mf b/org.springframework.jms/template.mf new file mode 100644 index 00000000000..04378c18d81 --- /dev/null +++ b/org.springframework.jms/template.mf @@ -0,0 +1,29 @@ +Bundle-SymbolicName: org.springframework.jms +Bundle-Name: Spring JMS +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Template: + javax.jms.*;version="[1.1.0, 2.0.0)", + javax.resource.*;version="[1.5.0, 2.0.0)";resolution:=optional, + javax.transaction.*;version="[1.0.1, 2.0.0)";resolution:=optional, + org.aopalliance.*;version="[1.0.0, 2.0.0)", + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.apache.commons.pool.*;version="[1.3.0, 2.0.0)";resolution:=optional, + org.springframework.aop.*;version="[3.0.0, 3.0.1)", + org.springframework.beans.*;version="[3.0.0, 3.0.1)", + org.springframework.context.*;version="[3.0.0, 3.0.1)", + org.springframework.core.*;version="[3.0.0, 3.0.1)", + org.springframework.jca.*;version="[3.0.0, 3.0.1)";resolution:=optional, + org.springframework.jndi.*;version="[3.0.0, 3.0.1)";resolution:=optional, + org.springframework.remoting.*;version="[3.0.0, 3.0.1)";resolution:=optional, + org.springframework.scheduling.*;version="[3.0.0, 3.0.1)";resolution:=optional, + org.springframework.transaction.*;version="[3.0.0, 3.0.1)";resolution:=optional, + org.springframework.util.*;version="[3.0.0, 3.0.1)" +Unversioned-Imports: + javax.naming.*, + org.w3c.dom.* +Ignored-Existing-Headers: + Bnd-LastModified, + Import-Package, + Export-Package, + Tool