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 @@
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 @@
+
+
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 @@
+
+
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 @@
+
+
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: + *
There are two solutions to the duplicate processing problem: + *
Recommendations: + *
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 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 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 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 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 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
+ * By default, an instance of Provides many configuration properties mirroring those of the Commons Pool
+ * 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 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 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 Default implementation simply calls Default implementation simply calls 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 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 @@
+
+ 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 The default implementation uses the
+ * The default implementation uses the
+ * 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 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 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 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
+ * 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
+ * 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 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 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
+ * 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 The only difference to the default SimpleMessageConverter is that BytesMessage
+ * is handled differently: namely, without using the Will lookup Spring managed beans identified by bean name,
+ * expecting them to be of type The BeanFactory to access must be set via 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 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 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 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
+The Spring Data Binding framework, an internal library used by Spring Web Flow.
+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.
+ *
+ * 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.
+ *
+ * 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.
+ * 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 @@
+
+close.
+ * ServerSessions in a configurable Jakarta Commons Pool.
+ *
+ * GenericObjectPool is created.
+ * Subclasses may change the type of ObjectPool used by
+ * overriding the createObjectPool method.
+ *
+ * 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).
+ * 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.
+ *
+ * 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.
+ * executeListener method.
+ * 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.
+ * 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.
+ * 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.
+ *
+ * javax.jms.QueueConnection.
+ * 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.
+ * 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.
+ * 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.
+ * 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.
+ *
+ * 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.
+ * 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 @@
+
+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.
+ * 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.
+ * 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.
+ *
+ * convertAndSend and receiveAndConvert operations.
+ *
+ * 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 @@
+
+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.
+ * setBeanFactory.
+ * @see #setBeanFactory
+ */
+ public BeanFactoryDestinationResolver() {
+ }
+
+ /**
+ * Create a new instance of the {@link BeanFactoryDestinationResolver} class.
+ * 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.
+ *
+ * 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.
+ *
+ * null).
+ */
+ public DestinationResolver getDestinationResolver() {
+ return this.destinationResolver;
+ }
+
+ /**
+ * Configure the destination accessor with knowledge of the JMS domain used.
+ * Default is Point-to-Point (Queues).
+ * 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 @@
+
+