/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.kurento.kmf.rabbitmq;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.AmqpIllegalStateException;
import org.springframework.amqp.core.Address;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.ReceiveAndReplyCallback;
import org.springframework.amqp.core.ReceiveAndReplyMessageCallback;
import org.springframework.amqp.core.ReplyToAddressCallback;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactoryUtils;
import org.springframework.amqp.rabbit.connection.RabbitAccessor;
import org.springframework.amqp.rabbit.connection.RabbitResourceHolder;
import org.springframework.amqp.rabbit.connection.RabbitUtils;
import org.springframework.amqp.rabbit.core.ChannelCallback;
import org.springframework.amqp.rabbit.core.RabbitOperations;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter;
import org.springframework.amqp.rabbit.support.MessagePropertiesConverter;
import org.springframework.amqp.rabbit.support.PendingConfirm;
import org.springframework.amqp.rabbit.support.PublisherCallbackChannel;
import org.springframework.amqp.rabbit.support.RabbitExceptionTranslator;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.amqp.support.converter.SimpleMessageConverter;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.AMQP.Queue.DeclareOk;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.GetResponse;
/**
* <p>
* Helper class that simplifies synchronous RabbitMQ access (sending and
* receiving messages).
* </p>
*
* <p>
* The default settings are for non-transactional messaging, which reduces the
* amount of data exchanged with the broker. To use a new transaction for every
* send or receive set the {@link #setChannelTransacted(boolean)
* channelTransacted} flag. To extend the transaction over multiple invocations
* (more efficient), you can use a Spring transaction to bracket the calls (with
* <code>channelTransacted=true</code> as well).
* </p>
*
* <p>
* The only mandatory property is the
* {@link #setConnectionFactory(ConnectionFactory) ConnectionFactory}. There are
* strategies available for converting messages to and from Java objects (
* {@link #setMessageConverter(MessageConverter) MessageConverter}) and for
* converting message headers (known as message properties in AMQP, see
* {@link #setMessagePropertiesConverter(MessagePropertiesConverter)
* MessagePropertiesConverter} ). The defaults probably do something sensible
* for typical use cases, as long as the message content-type is set
* appropriately.
* </p>
*
* <p>
* The "send" methods all have overloaded versions that allow you to explicitly
* target an exchange and a routing key, or you can set default values to be
* used in all send operations. The plain "receive" methods allow you to
* explicitly target a queue to receive from, or you can set a default value for
* the template that applies to all explicit receives. The convenience methods
* for send <b>and</b> receive use the sender defaults if no exchange or routing
* key is specified, but they always use a temporary queue for the receive leg,
* so the default queue is ignored.
* </p>
*
* @author Mark Pollack
* @author Mark Fisher
* @author Dave Syer
* @author Gary Russell
* @author Artem Bilan
* @since 1.0
*/
public class RabbitTemplate extends RabbitAccessor implements RabbitOperations,
MessageListener, PublisherCallbackChannel.Listener {
/** Alias for amq.direct default exchange */
private static final String DEFAULT_EXCHANGE = "";
private static final String DEFAULT_ROUTING_KEY = "";
private static final long DEFAULT_REPLY_TIMEOUT = 5000;
private static final String DEFAULT_ENCODING = "UTF-8";
private volatile String exchange = DEFAULT_EXCHANGE;
private volatile String routingKey = DEFAULT_ROUTING_KEY;
// The default queue name that will be used for synchronous receives.
private volatile String queue;
private volatile long replyTimeout = DEFAULT_REPLY_TIMEOUT;
private volatile MessageConverter messageConverter = new SimpleMessageConverter();
private volatile MessagePropertiesConverter messagePropertiesConverter = new DefaultMessagePropertiesConverter();
private volatile String encoding = DEFAULT_ENCODING;
private volatile Queue replyQueue;
private final Map<String, PendingReply> replyHolder = new ConcurrentHashMap<String, PendingReply>();
private volatile ConfirmCallback confirmCallback;
private volatile ReturnCallback returnCallback;
private final Map<Object, SortedMap<Long, PendingConfirm>> pendingConfirms = new ConcurrentHashMap<Object, SortedMap<Long, PendingConfirm>>();
private volatile boolean mandatory;
private final String uuid = UUID.randomUUID().toString();
private volatile String correlationKey = null;
private volatile RetryTemplate retryTemplate;
private final ReplyToAddressCallback<?> defaultReplyToAddressCallback = new ReplyToAddressCallback<Object>() {
@Override
public Address getReplyToAddress(Message request, Object reply) {
return RabbitTemplate.this.getReplyToAddress(request);
}
};
/**
* Convenient constructor for use with setter injection. Don't forget to set
* the connection factory.
*/
public RabbitTemplate() {
initDefaultStrategies();
}
/**
* Create a rabbit template with default strategies and settings.
*
* @param connectionFactory
* the connection factory to use
*/
public RabbitTemplate(ConnectionFactory connectionFactory) {
this();
setConnectionFactory(connectionFactory);
afterPropertiesSet();
}
/**
* Set up the default strategies. Subclasses can override if necessary.
*/
protected void initDefaultStrategies() {
setMessageConverter(new SimpleMessageConverter());
}
/**
* The name of the default exchange to use for send operations when none is
* specified. Defaults to <code>""</code> which is the default exchange in
* the broker (per the AMQP specification).
*
* @param exchange
* the exchange name to use for send operations
*/
public void setExchange(String exchange) {
this.exchange = (exchange != null) ? exchange : DEFAULT_EXCHANGE;
}
/**
* The value of a default routing key to use for send operations when none
* is specified. Default is empty which is not helpful when using the
* default (or any direct) exchange, but fine if the exchange is a headers
* exchange for instance.
*
* @param routingKey
* the default routing key to use for send operations
*/
public void setRoutingKey(String routingKey) {
this.routingKey = routingKey;
}
/**
* The name of the default queue to receive messages from when none is
* specified explicitly.
*
* @param queue
* the default queue name to use for receive
*/
public void setQueue(String queue) {
this.queue = queue;
}
/**
* The encoding to use when inter-converting between byte arrays and Strings
* in message properties.
*
* @param encoding
* the encoding to set
*/
public void setEncoding(String encoding) {
this.encoding = encoding;
}
/**
* A queue for replies; if not provided, a temporary exclusive, auto-delete
* queue will be used for each reply.
*
* @param replyQueue
* the replyQueue to set
*/
public void setReplyQueue(Queue replyQueue) {
this.replyQueue = replyQueue;
}
/**
* Specify the timeout in milliseconds to be used when waiting for a reply
* Message when using one of the sendAndReceive methods. The default value
* is defined as {@link #DEFAULT_REPLY_TIMEOUT}. A negative value indicates
* an indefinite timeout. Not used in the plain receive methods because
* there is no blocking receive operation defined in the protocol.
*
* @param replyTimeout
* the reply timeout in milliseconds
*
* @see #sendAndReceive(String, String, Message)
* @see #convertSendAndReceive(String, String, Object)
*/
public void setReplyTimeout(long replyTimeout) {
this.replyTimeout = replyTimeout;
}
/**
* Set the message converter for this template. Used to resolve Object
* parameters to convertAndSend methods and Object results from
* receiveAndConvert methods.
* <p>
* The default converter is a SimpleMessageConverter, which is able to
* handle byte arrays, Strings, and Serializable Objects depending on the
* message content type header.
*
* @param messageConverter
* The message converter.
*
* @see #convertAndSend
* @see #receiveAndConvert
* @see org.springframework.amqp.support.converter.SimpleMessageConverter
*/
public void setMessageConverter(MessageConverter messageConverter) {
this.messageConverter = messageConverter;
}
/**
* Set the {@link MessagePropertiesConverter} for this template. This
* converter is used to convert between raw byte content in the message
* headers and plain Java objects. In particular there are limitations when
* dealing with very long string headers, which hopefully are rare in
* practice, but if you need to use long headers you might need to inject a
* special converter here.
*
* @param messagePropertiesConverter
* The message properties converter.
*/
public void setMessagePropertiesConverter(
MessagePropertiesConverter messagePropertiesConverter) {
Assert.notNull(messagePropertiesConverter,
"messagePropertiesConverter must not be null");
this.messagePropertiesConverter = messagePropertiesConverter;
}
/**
* Return the message converter for this template. Useful for clients that
* want to take advantage of the converter in {@link ChannelCallback}
* implementations.
*
* @return The message converter.
*/
public MessageConverter getMessageConverter() {
return this.messageConverter;
}
public void setConfirmCallback(ConfirmCallback confirmCallback) {
Assert.state(this.confirmCallback == null
|| this.confirmCallback == confirmCallback,
"Only one ConfirmCallback is supported by each RabbitTemplate");
this.confirmCallback = confirmCallback;
}
public void setReturnCallback(ReturnCallback returnCallback) {
Assert.state(this.returnCallback == null
|| this.returnCallback == returnCallback,
"Only one ReturnCallback is supported by each RabbitTemplate");
this.returnCallback = returnCallback;
}
public void setMandatory(boolean mandatory) {
this.mandatory = mandatory;
}
/**
* If set to 'correlationId' (default) the correlationId property will be
* used; otherwise the supplied key will be used.
*
* @param correlationKey
* the correlationKey to set
*/
public void setCorrelationKey(String correlationKey) {
Assert.hasText(correlationKey,
"'correlationKey' must not be null or empty");
if (!correlationKey.trim().equals("correlationId")) {
this.correlationKey = correlationKey.trim();
}
}
/**
* Add a {@link RetryTemplate} which will be used for all rabbit operations.
*
* @param retryTemplate
* The retry template.
*/
public void setRetryTemplate(RetryTemplate retryTemplate) {
this.retryTemplate = retryTemplate;
}
/**
* Gets unconfirmed correlatiom data older than age and removes them.
*
* @param age
* in millseconds
* @return the collection of correlation data for which confirms have not
* been received.
*/
public Collection<CorrelationData> getUnconfirmed(long age) {
Set<CorrelationData> unconfirmed = new HashSet<CorrelationData>();
synchronized (this.pendingConfirms) {
long threshold = System.currentTimeMillis() - age;
for (Entry<Object, SortedMap<Long, PendingConfirm>> channelPendingConfirmEntry : this.pendingConfirms
.entrySet()) {
SortedMap<Long, PendingConfirm> channelPendingConfirms = channelPendingConfirmEntry
.getValue();
Iterator<Entry<Long, PendingConfirm>> iterator = channelPendingConfirms
.entrySet().iterator();
PendingConfirm pendingConfirm;
while (iterator.hasNext()) {
pendingConfirm = iterator.next().getValue();
if (pendingConfirm.getTimestamp() < threshold) {
unconfirmed.add(pendingConfirm.getCorrelationData());
iterator.remove();
} else {
break;
}
}
}
}
return unconfirmed.size() > 0 ? unconfirmed : null;
}
@Override
public void send(Message message) throws AmqpException {
send(this.exchange, this.routingKey, message);
}
@Override
public void send(String routingKey, Message message) throws AmqpException {
send(this.exchange, routingKey, message);
}
@Override
public void send(final String exchange, final String routingKey,
final Message message) throws AmqpException {
this.send(exchange, routingKey, message, null);
}
public void send(final String exchange, final String routingKey,
final Message message, final CorrelationData correlationData)
throws AmqpException {
execute(new ChannelCallback<Object>() {
@Override
public Object doInRabbit(Channel channel) throws Exception {
doSend(channel, exchange, routingKey, message, correlationData);
return null;
}
});
}
@Override
public void convertAndSend(Object object) throws AmqpException {
convertAndSend(this.exchange, this.routingKey, object,
(CorrelationData) null);
}
@Deprecated
public void correlationconvertAndSend(Object object,
CorrelationData correlationData) throws AmqpException {
this.correlationConvertAndSend(object, correlationData);
}
public void correlationConvertAndSend(Object object,
CorrelationData correlationData) throws AmqpException {
convertAndSend(this.exchange, this.routingKey, object, correlationData);
}
@Override
public void convertAndSend(String routingKey, final Object object)
throws AmqpException {
convertAndSend(this.exchange, routingKey, object,
(CorrelationData) null);
}
public void convertAndSend(String routingKey, final Object object,
CorrelationData correlationData) throws AmqpException {
convertAndSend(this.exchange, routingKey, object, correlationData);
}
@Override
public void convertAndSend(String exchange, String routingKey,
final Object object) throws AmqpException {
convertAndSend(exchange, routingKey, object, (CorrelationData) null);
}
public void convertAndSend(String exchange, String routingKey,
final Object object, CorrelationData correlationData)
throws AmqpException {
send(exchange, routingKey, convertMessageIfNecessary(object),
correlationData);
}
@Override
public void convertAndSend(Object message,
MessagePostProcessor messagePostProcessor) throws AmqpException {
convertAndSend(this.exchange, this.routingKey, message,
messagePostProcessor);
}
@Override
public void convertAndSend(String routingKey, Object message,
MessagePostProcessor messagePostProcessor) throws AmqpException {
convertAndSend(this.exchange, routingKey, message,
messagePostProcessor, null);
}
public void convertAndSend(String routingKey, Object message,
MessagePostProcessor messagePostProcessor,
CorrelationData correlationData) throws AmqpException {
convertAndSend(this.exchange, routingKey, message,
messagePostProcessor, correlationData);
}
@Override
public void convertAndSend(String exchange, String routingKey,
final Object message,
final MessagePostProcessor messagePostProcessor)
throws AmqpException {
convertAndSend(exchange, routingKey, message, messagePostProcessor,
null);
}
public void convertAndSend(String exchange, String routingKey,
final Object message,
final MessagePostProcessor messagePostProcessor,
CorrelationData correlationData) throws AmqpException {
Message messageToSend = convertMessageIfNecessary(message);
messageToSend = messagePostProcessor.postProcessMessage(messageToSend);
send(exchange, routingKey, messageToSend, correlationData);
}
@Override
public Message receive() throws AmqpException {
String queue = this.getRequiredQueue();
return this.receive(queue);
}
@Override
public Message receive(final String queueName) {
return execute(new ChannelCallback<Message>() {
@Override
public Message doInRabbit(Channel channel) throws IOException {
GetResponse response = channel.basicGet(queueName,
!isChannelTransacted());
// Response can be null is the case that there is no message on
// the queue.
if (response != null) {
long deliveryTag = response.getEnvelope().getDeliveryTag();
if (isChannelLocallyTransacted(channel)) {
channel.basicAck(deliveryTag, false);
channel.txCommit();
} else if (isChannelTransacted()) {
// Not locally transacted but it is transacted so it
// could be synchronized with an external transaction
ConnectionFactoryUtils.registerDeliveryTag(
getConnectionFactory(), channel, deliveryTag);
}
return RabbitTemplate.this
.buildMessageFromResponse(response);
}
return null;
}
});
}
@Override
public Object receiveAndConvert() throws AmqpException {
return receiveAndConvert(this.getRequiredQueue());
}
@Override
public Object receiveAndConvert(String queueName) throws AmqpException {
Message response = receive(queueName);
if (response != null) {
return getRequiredMessageConverter().fromMessage(response);
}
return null;
}
@Override
public <R, S> boolean receiveAndReply(ReceiveAndReplyCallback<R, S> callback)
throws AmqpException {
return this.receiveAndReply(this.getRequiredQueue(), callback);
}
@Override
@SuppressWarnings("unchecked")
public <R, S> boolean receiveAndReply(final String queueName,
ReceiveAndReplyCallback<R, S> callback) throws AmqpException {
return this.receiveAndReply(queueName, callback,
(ReplyToAddressCallback<S>) this.defaultReplyToAddressCallback);
}
@Override
public <R, S> boolean receiveAndReply(
ReceiveAndReplyCallback<R, S> callback, final String exchange,
final String routingKey) throws AmqpException {
return this.receiveAndReply(this.getRequiredQueue(), callback,
exchange, routingKey);
}
@Override
public <R, S> boolean receiveAndReply(final String queueName,
ReceiveAndReplyCallback<R, S> callback, final String replyExchange,
final String replyRoutingKey) throws AmqpException {
return this.receiveAndReply(queueName, callback,
new ReplyToAddressCallback<S>() {
@Override
public Address getReplyToAddress(Message request, S reply) {
return new Address(null, replyExchange, replyRoutingKey);
}
});
}
@Override
public <R, S> boolean receiveAndReply(
ReceiveAndReplyCallback<R, S> callback,
ReplyToAddressCallback<S> replyToAddressCallback)
throws AmqpException {
return this.receiveAndReply(this.getRequiredQueue(), callback,
replyToAddressCallback);
}
@Override
public <R, S> boolean receiveAndReply(String queueName,
ReceiveAndReplyCallback<R, S> callback,
ReplyToAddressCallback<S> replyToAddressCallback)
throws AmqpException {
return this.doReceiveAndReply(queueName, callback,
replyToAddressCallback);
}
@SuppressWarnings("unchecked")
private <R, S> boolean doReceiveAndReply(final String queueName,
final ReceiveAndReplyCallback<R, S> callback,
final ReplyToAddressCallback<S> replyToAddressCallback)
throws AmqpException {
return this.execute(new ChannelCallback<Boolean>() {
@Override
public Boolean doInRabbit(Channel channel) throws Exception {
boolean channelTransacted = RabbitTemplate.this
.isChannelTransacted();
GetResponse response = channel.basicGet(queueName,
!channelTransacted);
// Response can be null in the case that there is no message on
// the queue.
if (response != null) {
long deliveryTag = response.getEnvelope().getDeliveryTag();
boolean channelLocallyTransacted = RabbitTemplate.this
.isChannelLocallyTransacted(channel);
if (channelLocallyTransacted) {
channel.basicAck(deliveryTag, false);
} else if (channelTransacted) {
// Not locally transacted but it is transacted so it
// could be synchronized with an external transaction
ConnectionFactoryUtils.registerDeliveryTag(
RabbitTemplate.this.getConnectionFactory(),
channel, deliveryTag);
}
Message receiveMessage = RabbitTemplate.this
.buildMessageFromResponse(response);
Object receive = receiveMessage;
if (!(ReceiveAndReplyMessageCallback.class
.isAssignableFrom(callback.getClass()))) {
receive = RabbitTemplate.this
.getRequiredMessageConverter().fromMessage(
receiveMessage);
}
S reply;
try {
reply = callback.handle((R) receive);
} catch (ClassCastException e) {
StackTraceElement[] trace = e.getStackTrace();
if (trace[0].getMethodName().equals("handle")
&& trace[1].getFileName().equals(
"RabbitTemplate.java")) {
throw new IllegalArgumentException(
"ReceiveAndReplyCallback '"
+ callback
+ "' can't handle received object '"
+ receive + "'", e);
} else {
throw e;
}
}
if (reply != null) {
Address replyTo = replyToAddressCallback
.getReplyToAddress(receiveMessage, reply);
Message replyMessage = RabbitTemplate.this
.convertMessageIfNecessary(reply);
MessageProperties receiveMessageProperties = receiveMessage
.getMessageProperties();
MessageProperties replyMessageProperties = replyMessage
.getMessageProperties();
Object correlation = RabbitTemplate.this.correlationKey == null ? receiveMessageProperties
.getCorrelationId() : receiveMessageProperties
.getHeaders().get(
RabbitTemplate.this.correlationKey);
if (RabbitTemplate.this.correlationKey == null
|| correlation == null) {
// using standard correlationId property
if (correlation == null) {
String messageId = receiveMessageProperties
.getMessageId();
if (messageId != null) {
correlation = messageId
.getBytes(RabbitTemplate.this.encoding);
}
}
replyMessageProperties
.setCorrelationId((byte[]) correlation);
} else {
replyMessageProperties.setHeader(
RabbitTemplate.this.correlationKey,
correlation);
}
// 'doSend()' takes care about 'channel.txCommit()'.
RabbitTemplate.this.doSend(channel,
replyTo.getExchangeName(),
replyTo.getRoutingKey(), replyMessage, null);
} else if (channelLocallyTransacted) {
channel.txCommit();
}
return true;
}
return false;
}
});
}
@Override
public Message sendAndReceive(final Message message) throws AmqpException {
return this.doSendAndReceive(this.exchange, this.routingKey, message);
}
@Override
public Message sendAndReceive(final String routingKey, final Message message)
throws AmqpException {
return this.doSendAndReceive(this.exchange, routingKey, message);
}
@Override
public Message sendAndReceive(final String exchange,
final String routingKey, final Message message)
throws AmqpException {
return this.doSendAndReceive(exchange, routingKey, message);
}
@Override
public Object convertSendAndReceive(final Object message)
throws AmqpException {
return this.convertSendAndReceive(this.exchange, this.routingKey,
message, null);
}
@Override
public Object convertSendAndReceive(final String routingKey,
final Object message) throws AmqpException {
return this.convertSendAndReceive(this.exchange, routingKey, message,
null);
}
@Override
public Object convertSendAndReceive(final String exchange,
final String routingKey, final Object message) throws AmqpException {
return this.convertSendAndReceive(exchange, routingKey, message, null);
}
@Override
public Object convertSendAndReceive(final Object message,
final MessagePostProcessor messagePostProcessor)
throws AmqpException {
return this.convertSendAndReceive(this.exchange, this.routingKey,
message, messagePostProcessor);
}
@Override
public Object convertSendAndReceive(final String routingKey,
final Object message,
final MessagePostProcessor messagePostProcessor)
throws AmqpException {
return this.convertSendAndReceive(this.exchange, routingKey, message,
messagePostProcessor);
}
@Override
public Object convertSendAndReceive(final String exchange,
final String routingKey, final Object message,
final MessagePostProcessor messagePostProcessor)
throws AmqpException {
Message requestMessage = convertMessageIfNecessary(message);
if (messagePostProcessor != null) {
requestMessage = messagePostProcessor
.postProcessMessage(requestMessage);
}
Message replyMessage = this.doSendAndReceive(exchange, routingKey,
requestMessage);
if (replyMessage == null) {
return null;
}
return this.getRequiredMessageConverter().fromMessage(replyMessage);
}
protected Message convertMessageIfNecessary(final Object object) {
if (object instanceof Message) {
return (Message) object;
}
return getRequiredMessageConverter().toMessage(object,
new MessageProperties());
}
/**
* Send a message and wait for a reply.
*
* @param exchange
* the exchange name
* @param routingKey
* the routing key
* @param message
* the message to send
* @return the message that is received in reply
*/
protected Message doSendAndReceive(final String exchange,
final String routingKey, final Message message) {
if (this.replyQueue == null) {
return doSendAndReceiveWithTemporary(exchange, routingKey, message);
} else {
return doSendAndReceiveWithFixed(exchange, routingKey, message);
}
}
protected Message doSendAndReceiveWithTemporary(final String exchange,
final String routingKey, final Message message) {
return this.execute(new ChannelCallback<Message>() {
@Override
public Message doInRabbit(Channel channel) throws Exception {
final ArrayBlockingQueue<Message> replyHandoff = new ArrayBlockingQueue<Message>(
1);
Assert.isNull(
message.getMessageProperties().getReplyTo(),
"Send-and-receive methods can only be used if the Message does not already have a replyTo property.");
DeclareOk queueDeclaration = channel.queueDeclare();
String replyTo = queueDeclaration.getQueue();
message.getMessageProperties().setReplyTo(replyTo);
String consumerTag = UUID.randomUUID().toString();
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope, AMQP.BasicProperties properties,
byte[] body) throws IOException {
MessageProperties messageProperties = messagePropertiesConverter
.toMessageProperties(properties, envelope,
encoding);
Message reply = new Message(body, messageProperties);
if (logger.isTraceEnabled()) {
logger.trace("Message received " + reply);
}
try {
replyHandoff.put(reply);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
};
channel.basicConsume(replyTo, true, consumerTag, true, true,
null, consumer);
doSend(channel, exchange, routingKey, message, null);
Message reply = (replyTimeout < 0) ? replyHandoff.take()
: replyHandoff
.poll(replyTimeout, TimeUnit.MILLISECONDS);
channel.basicCancel(consumerTag);
return reply;
}
});
}
protected Message doSendAndReceiveWithFixed(final String exchange,
final String routingKey, final Message message) {
return this.execute(new ChannelCallback<Message>() {
@Override
public Message doInRabbit(Channel channel) throws Exception {
final PendingReply pendingReply = new PendingReply();
byte[] messageTagBytes = message.getMessageProperties()
.getCorrelationId();
String messageTag;
if (messageTagBytes != null) {
messageTag = new String(messageTagBytes);
} else {
messageTag = UUID.randomUUID().toString();
}
RabbitTemplate.this.replyHolder.put(messageTag, pendingReply);
// Save any existing replyTo and correlation data
String savedReplyTo = message.getMessageProperties()
.getReplyTo();
pendingReply.setSavedReplyTo(savedReplyTo);
if (StringUtils.hasLength(savedReplyTo)
&& logger.isDebugEnabled()) {
logger.debug("Replacing replyTo header:" + savedReplyTo
+ " in favor of template's configured reply-queue:"
+ RabbitTemplate.this.replyQueue.getName());
}
message.getMessageProperties().setReplyTo(
RabbitTemplate.this.replyQueue.getName());
String savedCorrelation = null;
if (RabbitTemplate.this.correlationKey == null) { // using
// standard
// correlationId
// property
byte[] correlationId = message.getMessageProperties()
.getCorrelationId();
if (correlationId != null) {
savedCorrelation = new String(correlationId,
RabbitTemplate.this.encoding);
}
} else {
savedCorrelation = (String) message.getMessageProperties()
.getHeaders()
.get(RabbitTemplate.this.correlationKey);
}
pendingReply.setSavedCorrelation(savedCorrelation);
if (RabbitTemplate.this.correlationKey == null) { // using
// standard
// correlationId
// property
message.getMessageProperties().setCorrelationId(
messageTag.getBytes(RabbitTemplate.this.encoding));
} else {
message.getMessageProperties().setHeader(
RabbitTemplate.this.correlationKey, messageTag);
}
if (logger.isDebugEnabled()) {
logger.debug("Sending message with tag " + messageTag);
}
doSend(channel, exchange, routingKey, message, null);
LinkedBlockingQueue<Message> replyHandoff = pendingReply
.getQueue();
Message reply = (replyTimeout < 0) ? replyHandoff.take()
: replyHandoff
.poll(replyTimeout, TimeUnit.MILLISECONDS);
RabbitTemplate.this.replyHolder.remove(messageTag);
return reply;
}
});
}
@Override
public <T> T execute(final ChannelCallback<T> action) {
if (this.retryTemplate != null) {
try {
return this.retryTemplate
.execute(new RetryCallback<T, Exception>() {
@Override
public T doWithRetry(RetryContext context)
throws Exception {
return RabbitTemplate.this.doExecute(action);
}
});
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw (RuntimeException) e;
}
throw RabbitExceptionTranslator.convertRabbitAccessException(e);
}
} else {
return this.doExecute(action);
}
}
private <T> T doExecute(ChannelCallback<T> action) {
Assert.notNull(action, "Callback object must not be null");
RabbitResourceHolder resourceHolder = getTransactionalResourceHolder();
Channel channel = resourceHolder.getChannel();
if (this.confirmCallback != null || this.returnCallback != null) {
addListener(channel);
}
try {
if (logger.isDebugEnabled()) {
logger.debug("Executing callback on RabbitMQ Channel: "
+ channel);
}
return action.doInRabbit(channel);
} catch (Exception ex) {
if (isChannelLocallyTransacted(channel)) {
resourceHolder.rollbackAll();
}
throw convertRabbitAccessException(ex);
} finally {
ConnectionFactoryUtils.releaseResources(resourceHolder);
}
}
/**
* Send the given message to the specified exchange.
*
* @param channel
* The RabbitMQ Channel to operate within.
* @param exchange
* The name of the RabbitMQ exchange to send to.
* @param routingKey
* The routing key.
* @param message
* The Message to send.
* @param correlationData
* The correlation data.
* @throws IOException
* If thrown by RabbitMQ API methods
*/
protected void doSend(Channel channel, String exchange, String routingKey,
Message message, CorrelationData correlationData) throws Exception {
if (logger.isDebugEnabled()) {
logger.debug("Publishing message on exchange [" + exchange
+ "], routingKey = [" + routingKey + "]");
}
if (exchange == null) {
// try to send to configured exchange
exchange = this.exchange;
}
if (routingKey == null) {
// try to send to configured routing key
routingKey = this.routingKey;
}
if (this.confirmCallback != null
&& channel instanceof PublisherCallbackChannel) {
PublisherCallbackChannel publisherCallbackChannel = (PublisherCallbackChannel) channel;
publisherCallbackChannel.addPendingConfirm(this, channel
.getNextPublishSeqNo(), new PendingConfirm(correlationData,
System.currentTimeMillis()));
}
boolean mandatory = this.returnCallback != null && this.mandatory;
MessageProperties messageProperties = message.getMessageProperties();
if (mandatory) {
messageProperties.getHeaders().put(
PublisherCallbackChannel.RETURN_CORRELATION, this.uuid);
}
BasicProperties convertedMessageProperties = this.messagePropertiesConverter
.fromMessageProperties(messageProperties, encoding);
channel.basicPublish(exchange, routingKey, mandatory,
convertedMessageProperties, message.getBody());
// Check if commit needed
if (isChannelLocallyTransacted(channel)) {
// Transacted channel created by this template -> commit.
RabbitUtils.commitIfNecessary(channel);
}
}
/**
* Check whether the given Channel is locally transacted, that is, whether
* its transaction is managed by this template's Channel handling and not by
* an external transaction coordinator.
*
* @param channel
* the Channel to check
* @return whether the given Channel is locally transacted
* @see ConnectionFactoryUtils#isChannelTransactional
* @see #isChannelTransacted
*/
protected boolean isChannelLocallyTransacted(Channel channel) {
return isChannelTransacted()
&& !ConnectionFactoryUtils.isChannelTransactional(channel,
getConnectionFactory());
}
private Message buildMessageFromResponse(GetResponse response) {
MessageProperties messageProps = this.messagePropertiesConverter
.toMessageProperties(response.getProps(),
response.getEnvelope(), this.encoding);
messageProps.setMessageCount(response.getMessageCount());
return new Message(response.getBody(), messageProps);
}
private MessageConverter getRequiredMessageConverter()
throws IllegalStateException {
MessageConverter converter = this.getMessageConverter();
if (converter == null) {
throw new AmqpIllegalStateException(
"No 'messageConverter' specified. Check configuration of RabbitTemplate.");
}
return converter;
}
private String getRequiredQueue() throws IllegalStateException {
String name = this.queue;
if (name == null) {
throw new AmqpIllegalStateException(
"No 'queue' specified. Check configuration of RabbitTemplate.");
}
return name;
}
/**
* Determine a reply-to Address for the given message.
* <p>
* The default implementation first checks the Rabbit Reply-To Address of
* the supplied request; if that is not <code>null</code> it is returned; if
* it is <code>null</code>, then the configured default Exchange and routing
* key are used to construct a reply-to Address. If the exchange property is
* also <code>null</code>, then an {@link AmqpException} is thrown.
*
* @param request
* the original incoming Rabbit message
* @return the reply-to Address (never <code>null</code>)
* @throws AmqpException
* if no {@link Address} can be determined
* @see org.springframework.amqp.core.Message#getMessageProperties()
* @see org.springframework.amqp.core.MessageProperties#getReplyTo()
*/
private Address getReplyToAddress(Message request) throws AmqpException {
Address replyTo = request.getMessageProperties().getReplyToAddress();
if (replyTo == null) {
if (this.exchange == null) {
throw new AmqpException(
"Cannot determine ReplyTo message property value: "
+ "Request message does not contain reply-to property, and no default Exchange was set.");
}
replyTo = new Address(null, this.exchange, this.routingKey);
}
return replyTo;
}
private void addListener(Channel channel) {
if (channel instanceof PublisherCallbackChannel) {
PublisherCallbackChannel publisherCallbackChannel = (PublisherCallbackChannel) channel;
SortedMap<Long, PendingConfirm> pendingConfirms = publisherCallbackChannel
.addListener(this);
if (!this.pendingConfirms.containsKey(channel)) {
this.pendingConfirms.put(channel, pendingConfirms);
if (logger.isDebugEnabled()) {
logger.debug("Added pending confirms for " + channel
+ " to map, size now "
+ this.pendingConfirms.size());
}
}
} else {
throw new IllegalStateException(
"Channel does not support confirms or returns; "
+ "is the connection factory configured for confirms or returns?");
}
}
@Override
public void handleConfirm(PendingConfirm pendingConfirm, boolean ack) {
if (this.confirmCallback != null) {
this.confirmCallback.confirm(pendingConfirm.getCorrelationData(),
ack);
} else {
if (logger.isDebugEnabled()) {
logger.warn("Confirm received but no callback available");
}
}
}
@Override
public void handleReturn(int replyCode, String replyText, String exchange,
String routingKey, BasicProperties properties, byte[] body)
throws IOException {
if (this.returnCallback == null) {
if (logger.isWarnEnabled()) {
logger.warn("Returned message but no callback available");
}
} else {
properties.getHeaders().remove(
PublisherCallbackChannel.RETURN_CORRELATION);
MessageProperties messageProperties = messagePropertiesConverter
.toMessageProperties(properties, null, this.encoding);
Message returnedMessage = new Message(body, messageProperties);
this.returnCallback.returnedMessage(returnedMessage, replyCode,
replyText, exchange, routingKey);
}
}
@Override
public boolean isConfirmListener() {
return this.confirmCallback != null;
}
@Override
public boolean isReturnListener() {
return this.returnCallback != null;
}
@Override
public void removePendingConfirmsReference(Channel channel,
SortedMap<Long, PendingConfirm> unconfirmed) {
this.pendingConfirms.remove(channel);
if (logger.isDebugEnabled()) {
logger.debug("Removed pending confirms for " + channel
+ " from map, size now " + this.pendingConfirms.size());
}
}
@Override
public String getUUID() {
return this.uuid;
}
@Override
public void onMessage(Message message) {
try {
String messageTag;
if (this.correlationKey == null) { // using standard correlationId
// property
messageTag = new String(message.getMessageProperties()
.getCorrelationId(), this.encoding);
} else {
messageTag = (String) message.getMessageProperties()
.getHeaders().get(this.correlationKey);
}
if (messageTag == null) {
logger.error("No correlation header in reply");
return;
}
PendingReply pendingReply = this.replyHolder.get(messageTag);
if (pendingReply == null) {
if (logger.isWarnEnabled()) {
logger.warn("Reply received after timeout for "
+ messageTag);
}
} else {
// Restore the inbound correlation data
String savedCorrelation = pendingReply.getSavedCorrelation();
if (this.correlationKey == null) {
if (savedCorrelation == null) {
message.getMessageProperties().setCorrelationId(null);
} else {
message.getMessageProperties().setCorrelationId(
savedCorrelation.getBytes(this.encoding));
}
} else {
if (savedCorrelation != null) {
message.getMessageProperties().setHeader(
this.correlationKey, savedCorrelation);
} else {
message.getMessageProperties().getHeaders()
.remove(this.correlationKey);
}
}
// Restore any inbound replyTo
String savedReplyTo = pendingReply.getSavedReplyTo();
message.getMessageProperties().setReplyTo(savedReplyTo);
LinkedBlockingQueue<Message> queue = pendingReply.getQueue();
queue.add(message);
if (logger.isDebugEnabled()) {
logger.debug("Reply received for " + messageTag);
if (savedReplyTo != null) {
logger.debug("Restored replyTo to " + savedReplyTo);
}
}
}
} catch (UnsupportedEncodingException e) {
throw new AmqpIllegalStateException("Invalid Character Set:"
+ this.encoding, e);
}
}
private static class PendingReply {
private volatile String savedReplyTo;
private volatile String savedCorrelation;
private final LinkedBlockingQueue<Message> queue;
public PendingReply() {
this.queue = new LinkedBlockingQueue<Message>();
}
public String getSavedReplyTo() {
return savedReplyTo;
}
public void setSavedReplyTo(String savedReplyTo) {
this.savedReplyTo = savedReplyTo;
}
public String getSavedCorrelation() {
return savedCorrelation;
}
public void setSavedCorrelation(String savedCorrelation) {
this.savedCorrelation = savedCorrelation;
}
public LinkedBlockingQueue<Message> getQueue() {
return queue;
}
}
public interface ConfirmCallback {
void confirm(CorrelationData correlationData, boolean ack);
}
public interface ReturnCallback {
void returnedMessage(Message message, int replyCode, String replyText,
String exchange, String routingKey);
}
}