/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.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.integration.jms;
import java.util.Map;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.InvalidDestinationException;
import javax.jms.JMSException;
import javax.jms.MessageProducer;
import javax.jms.Session;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.integration.gateway.MessagingGatewaySupport;
import org.springframework.integration.support.DefaultMessageBuilderFactory;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.integration.support.management.TrackableComponent;
import org.springframework.integration.support.utils.IntegrationUtils;
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.jms.support.destination.DestinationResolver;
import org.springframework.jms.support.destination.DynamicDestinationResolver;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.util.Assert;
/**
* JMS MessageListener that converts a JMS Message into a Spring Integration
* Message and sends that Message to a channel. If the 'expectReply' value is
* <code>true</code>, it will also wait for a Spring Integration reply Message
* and convert that into a JMS reply.
*
* @author Mark Fisher
* @author Juergen Hoeller
* @author Oleg Zhurakousky
* @author Artem Bilan
* @author Gary Russell
*/
public class ChannelPublishingJmsMessageListener
implements SessionAwareMessageListener<javax.jms.Message>, InitializingBean,
TrackableComponent, BeanFactoryAware {
protected final Log logger = LogFactory.getLog(getClass());
private volatile boolean expectReply;
private volatile MessageConverter messageConverter = new SimpleMessageConverter();
private volatile boolean extractRequestPayload = true;
private volatile boolean extractReplyPayload = true;
private volatile Object defaultReplyDestination;
private volatile String correlationKey;
private volatile long replyTimeToLive = javax.jms.Message.DEFAULT_TIME_TO_LIVE;
private volatile int replyPriority = javax.jms.Message.DEFAULT_PRIORITY;
private volatile int replyDeliveryMode = javax.jms.Message.DEFAULT_DELIVERY_MODE;
private volatile boolean explicitQosEnabledForReplies;
private volatile DestinationResolver destinationResolver = new DynamicDestinationResolver();
private volatile JmsHeaderMapper headerMapper = new DefaultJmsHeaderMapper();
private final GatewayDelegate gatewayDelegate = new GatewayDelegate();
private volatile BeanFactory beanFactory;
private volatile MessageBuilderFactory messageBuilderFactory = new DefaultMessageBuilderFactory();
/**
* Specify whether a JMS reply Message is expected.
* @param expectReply true if a reply is expected.
*/
public void setExpectReply(boolean expectReply) {
this.expectReply = expectReply;
}
public void setComponentName(String componentName) {
this.gatewayDelegate.setComponentName(componentName);
}
public void setRequestChannel(MessageChannel requestChannel) {
this.gatewayDelegate.setRequestChannel(requestChannel);
}
public void setRequestChannelName(String requestChannelName) {
this.gatewayDelegate.setRequestChannelName(requestChannelName);
}
public void setReplyChannel(MessageChannel replyChannel) {
this.gatewayDelegate.setReplyChannel(replyChannel);
}
public void setReplyChannelName(String replyChannelName) {
this.gatewayDelegate.setReplyChannelName(replyChannelName);
}
public void setErrorChannel(MessageChannel errorChannel) {
this.gatewayDelegate.setErrorChannel(errorChannel);
}
public void setErrorChannelName(String errorChannelName) {
this.gatewayDelegate.setErrorChannelName(errorChannelName);
}
public void setRequestTimeout(long requestTimeout) {
this.gatewayDelegate.setRequestTimeout(requestTimeout);
}
public void setReplyTimeout(long replyTimeout) {
this.gatewayDelegate.setReplyTimeout(replyTimeout);
}
@Override
public void setShouldTrack(boolean shouldTrack) {
this.gatewayDelegate.setShouldTrack(shouldTrack);
}
@Override
public String getComponentName() {
return this.gatewayDelegate.getComponentName();
}
@Override
public String getComponentType() {
return this.gatewayDelegate.getComponentType();
}
/**
* Set the default reply destination to send reply messages to. This will
* be applied in case of a request message that does not carry a
* "JMSReplyTo" field.
* @param defaultReplyDestination The default reply destination.
*/
public void setDefaultReplyDestination(Destination defaultReplyDestination) {
this.defaultReplyDestination = defaultReplyDestination;
}
/**
* Set the name of the default reply queue to send reply messages to.
* This will be applied in case of a request message that does not carry a
* "JMSReplyTo" field.
* <p>Alternatively, specify a JMS Destination object as "defaultReplyDestination".
* @param destinationName The default reply destination name.
* @see #setDestinationResolver
* @see #setDefaultReplyDestination(javax.jms.Destination)
*/
public void setDefaultReplyQueueName(String destinationName) {
this.defaultReplyDestination = new DestinationNameHolder(destinationName, false);
}
/**
* Set the name of the default reply topic to send reply messages to.
* This will be applied in case of a request message that does not carry a
* "JMSReplyTo" field.
* <p>Alternatively, specify a JMS Destination object as "defaultReplyDestination".
* @param destinationName The default reply topic name.
* @see #setDestinationResolver
* @see #setDefaultReplyDestination(javax.jms.Destination)
*/
public void setDefaultReplyTopicName(String destinationName) {
this.defaultReplyDestination = new DestinationNameHolder(destinationName, true);
}
/**
* Specify the time-to-live property for JMS reply Messages.
* @param replyTimeToLive The reply time to live.
* @see javax.jms.MessageProducer#setTimeToLive(long)
*/
public void setReplyTimeToLive(long replyTimeToLive) {
this.replyTimeToLive = replyTimeToLive;
}
/**
* Specify the priority value for JMS reply Messages.
* @param replyPriority The reply priority.
* @see javax.jms.MessageProducer#setPriority(int)
*/
public void setReplyPriority(int replyPriority) {
this.replyPriority = replyPriority;
}
/**
* Specify the delivery mode for JMS reply Messages.
* @param replyDeliveryPersistent true for a persistent reply message.
* @see javax.jms.MessageProducer#setDeliveryMode(int)
*/
public void setReplyDeliveryPersistent(boolean replyDeliveryPersistent) {
this.replyDeliveryMode = replyDeliveryPersistent ? DeliveryMode.PERSISTENT : DeliveryMode.NON_PERSISTENT;
}
/**
* Provide the name of a JMS property that should be copied from the request
* Message to the reply Message. If this value is NULL (the default) then the
* JMSMessageID from the request will be copied into the JMSCorrelationID of the reply
* unless there is already a value in the JMSCorrelationID property of the newly created
* reply Message in which case nothing will be copied. If the JMSCorrelationID of the
* request Message should be copied into the JMSCorrelationID of the reply Message
* instead, then this value should be set to "JMSCorrelationID".
* Any other value will be treated as a JMS String Property to be copied as-is
* from the request Message into the reply Message with the same property name.
* @param correlationKey The correlation key.
*/
public void setCorrelationKey(String correlationKey) {
this.correlationKey = correlationKey;
}
/**
* Specify whether explicit QoS should be enabled for replies
* (for timeToLive, priority, and deliveryMode settings).
* @param explicitQosEnabledForReplies true to enable explicit QoS.
*/
public void setExplicitQosEnabledForReplies(boolean explicitQosEnabledForReplies) {
this.explicitQosEnabledForReplies = explicitQosEnabledForReplies;
}
/**
* Set the DestinationResolver that should be used to resolve reply
* destination names for this listener.
* <p>The default resolver is a DynamicDestinationResolver. Specify a
* JndiDestinationResolver for resolving destination names as JNDI locations.
* @param destinationResolver The destination resolver.
* @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;
}
/**
* Provide a {@link MessageConverter} implementation to use when
* converting between JMS Messages and Spring Integration Messages.
* If none is provided, a {@link SimpleMessageConverter} will
* be used.
* @param messageConverter The message converter.
*/
public void setMessageConverter(MessageConverter messageConverter) {
this.messageConverter = messageConverter;
}
/**
* Provide a {@link JmsHeaderMapper} implementation to use when
* converting between JMS Messages and Spring Integration Messages.
* If none is provided, a {@link DefaultJmsHeaderMapper} will be used.
* @param headerMapper The header mapper.
*/
public void setHeaderMapper(JmsHeaderMapper headerMapper) {
this.headerMapper = headerMapper;
}
/**
* Specify whether the JMS request Message's body should be extracted prior
* to converting into a Spring Integration Message. This value is set to
* <code>true</code> by default. To send the JMS Message itself as a
* Spring Integration Message payload, set this to <code>false</code>.
* @param extractRequestPayload true if the request payload should be extracted.
*/
public void setExtractRequestPayload(boolean extractRequestPayload) {
this.extractRequestPayload = extractRequestPayload;
}
/**
* Specify whether the Spring Integration reply Message's payload should be
* extracted prior to converting into a JMS Message. This value is set to
* <code>true</code> by default. To send the Spring Integration Message
* itself as the JMS Message's body, set this to <code>false</code>.
* @param extractReplyPayload true if the reply payload should be extracted.
*/
public void setExtractReplyPayload(boolean extractReplyPayload) {
this.extractReplyPayload = extractReplyPayload;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void onMessage(javax.jms.Message jmsMessage, Session session) throws JMSException {
Object result = jmsMessage;
Message<?> requestMessage = null;
boolean errors = false;
try {
if (this.extractRequestPayload) {
result = this.messageConverter.fromMessage(jmsMessage);
if (this.logger.isDebugEnabled()) {
this.logger.debug("converted JMS Message [" + jmsMessage + "] to integration Message payload ["
+ result + "]");
}
}
Map<String, Object> headers = this.headerMapper.toHeaders(jmsMessage);
requestMessage = (result instanceof Message<?>) ?
this.messageBuilderFactory.fromMessage((Message<?>) result).copyHeaders(headers).build() :
this.messageBuilderFactory.withPayload(result).copyHeaders(headers).build();
}
catch (RuntimeException e) {
MessageChannel errorChannel = this.gatewayDelegate.getErrorChannel();
if (errorChannel == null) {
throw e;
}
errorChannel.send(this.gatewayDelegate.buildErrorMessage(
new MessagingException("Inbound conversion failed for: " + jmsMessage, e)));
errors = true;
}
if (!errors) {
if (!this.expectReply) {
this.gatewayDelegate.send(requestMessage);
}
else {
Message<?> replyMessage = this.gatewayDelegate.sendAndReceiveMessage(requestMessage);
if (replyMessage != null) {
Destination destination = this.getReplyDestination(jmsMessage, session);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Reply destination: " + destination);
}
if (destination != null) {
// convert SI Message to JMS Message
Object replyResult = replyMessage;
if (this.extractReplyPayload) {
replyResult = replyMessage.getPayload();
}
try {
javax.jms.Message jmsReply = this.messageConverter.toMessage(replyResult, session);
// map SI Message Headers to JMS Message Properties/Headers
this.headerMapper.fromHeaders(replyMessage.getHeaders(), jmsReply);
this.copyCorrelationIdFromRequestToReply(jmsMessage, jmsReply);
this.sendReply(jmsReply, destination, session);
}
catch (RuntimeException e) {
this.logger.error("Failed to generate JMS Reply Message from: " + replyResult, e);
throw e;
}
}
}
else if (this.logger.isDebugEnabled()) {
this.logger.debug("expected a reply but none was received");
}
}
}
}
@Override
public void afterPropertiesSet() {
if (this.beanFactory != null) {
this.gatewayDelegate.setBeanFactory(this.beanFactory);
}
this.gatewayDelegate.afterPropertiesSet();
this.messageBuilderFactory = IntegrationUtils.getMessageBuilderFactory(this.beanFactory);
}
protected void start() {
this.gatewayDelegate.start();
}
protected void stop() {
this.gatewayDelegate.stop();
}
private void copyCorrelationIdFromRequestToReply(javax.jms.Message requestMessage, javax.jms.Message replyMessage)
throws JMSException {
if (this.correlationKey != null) {
if (this.correlationKey.equals("JMSCorrelationID")) {
replyMessage.setJMSCorrelationID(requestMessage.getJMSCorrelationID());
}
else {
String value = requestMessage.getStringProperty(this.correlationKey);
if (value != null) {
replyMessage.setStringProperty(this.correlationKey, value);
}
else if (this.logger.isWarnEnabled()) {
this.logger.warn("No property value available on request Message for correlationKey '"
+ this.correlationKey + "'");
}
}
}
else if (replyMessage.getJMSCorrelationID() == null) {
replyMessage.setJMSCorrelationID(requestMessage.getJMSMessageID());
}
}
/**
* Determine a reply destination for the given message.
* <p>
* This implementation first checks the boolean 'error' flag which signifies that the reply is an error message. If
* reply is not an error it will first check the JMS Reply-To {@link Destination} of the supplied request message;
* if that is not <code>null</code> it is returned; if it is <code>null</code>, then the configured
* {@link #resolveDefaultReplyDestination default reply destination} is returned; if this too is <code>null</code>,
* then an {@link InvalidDestinationException} is thrown.
* @param request the original incoming JMS message
* @param session the JMS Session to operate on
* @return the reply destination (never <code>null</code>)
* @throws JMSException if thrown by JMS API methods
* @throws InvalidDestinationException if no {@link Destination} can be determined
* @see #setDefaultReplyDestination
* @see javax.jms.Message#getJMSReplyTo()
*/
private Destination getReplyDestination(javax.jms.Message request, Session session) throws JMSException {
Destination replyTo = request.getJMSReplyTo();
if (replyTo == null) {
replyTo = resolveDefaultReplyDestination(session);
if (replyTo == null) {
throw new InvalidDestinationException("Cannot determine reply destination: " +
"Request message does not contain reply-to destination, and no default reply destination set.");
}
}
return replyTo;
}
/**
* Resolve the default reply destination into a JMS {@link Destination}, using this
* listener's {@link DestinationResolver} in case of a destination name.
* @param session The session.
* @return the located {@link Destination}
* @throws javax.jms.JMSException if resolution failed
* @see #setDefaultReplyDestination
* @see #setDefaultReplyQueueName
* @see #setDefaultReplyTopicName
* @see #setDestinationResolver
*/
private Destination resolveDefaultReplyDestination(Session session) throws JMSException {
if (this.defaultReplyDestination instanceof Destination) {
return (Destination) this.defaultReplyDestination;
}
if (this.defaultReplyDestination instanceof DestinationNameHolder) {
DestinationNameHolder nameHolder = (DestinationNameHolder) this.defaultReplyDestination;
return this.destinationResolver.resolveDestinationName(session, nameHolder.name, nameHolder.isTopic);
}
return null;
}
private void sendReply(javax.jms.Message replyMessage, Destination destination, Session session)
throws JMSException {
MessageProducer producer = session.createProducer(destination);
try {
if (this.explicitQosEnabledForReplies) {
producer.send(replyMessage, this.replyDeliveryMode, this.replyPriority, this.replyTimeToLive);
}
else {
producer.send(replyMessage);
}
}
finally {
JmsUtils.closeMessageProducer(producer);
}
}
/**
* Internal class combining a destination name
* and its target destination type (queue or topic).
*/
private static final class DestinationNameHolder {
private final String name;
private final boolean isTopic;
DestinationNameHolder(String name, boolean isTopic) {
this.name = name;
this.isTopic = isTopic;
}
}
private class GatewayDelegate extends MessagingGatewaySupport {
GatewayDelegate() {
super();
}
@Override
public MessageChannel getErrorChannel() {
return super.getErrorChannel();
}
@Override
protected void send(Object request) {
super.send(request);
}
@Override
protected Message<?> sendAndReceiveMessage(Object request) {
return super.sendAndReceiveMessage(request);
}
public ErrorMessage buildErrorMessage(Throwable throwable) {
return super.buildErrorMessage(null, throwable);
}
@Override
public String getComponentType() {
if (ChannelPublishingJmsMessageListener.this.expectReply) {
return "jms:inbound-gateway";
}
else {
return "jms:message-driven-channel-adapter";
}
}
}
}