/**
* Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
*
* WSO2 Inc. licenses this file to you 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.wso2.carbon.inbound.endpoint.protocol.rabbitmq;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConsumerCancelledException;
import com.rabbitmq.client.ShutdownSignalException;
import com.rabbitmq.client.QueueingConsumer;
import org.apache.axiom.om.OMException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Map;
import java.util.Properties;
public class RabbitMQConnectionConsumer {
private static final Log log = LogFactory.getLog(RabbitMQConnectionConsumer.class);
private RabbitMQConnectionFactory rabbitMQConnectionFactory;
private Properties rabbitMQProperties;
private static final int STATE_STOPPED = 0;
private static final int STATE_STARTED = 1;
private static final int STATE_PAUSED = 2;
private static final int STATE_SHUTTING_DOWN = 3;
private static final int STATE_FAILURE = 4;
private static final int STATE_FAULTY = 5;
private volatile int workerState = STATE_STOPPED;
private String inboundName;
private Connection connection = null;
private Channel channel = null;
private boolean autoAck = false;
private QueueingConsumer queueingConsumer;
private String queueName, routeKey, exchangeName;
private Hashtable<String, String> rabbitMQProps = new Hashtable<>();
private RabbitMQInjectHandler injectHandler;
private String consumerTagString;
private volatile boolean connected = false;
private volatile boolean idle = false;
public RabbitMQConnectionConsumer(RabbitMQConnectionFactory rabbitMQConnectionFactory, Properties rabbitMQProperties, RabbitMQInjectHandler injectHandler) {
this.rabbitMQConnectionFactory = rabbitMQConnectionFactory;
this.rabbitMQProperties = rabbitMQProperties;
this.injectHandler = injectHandler;
for (final String propertyName : rabbitMQProperties.stringPropertyNames()) {
this.rabbitMQProps.put(propertyName, rabbitMQProperties.getProperty(propertyName));
}
}
public void execute() {
try {
workerState = STATE_STARTED;
initConsumer();
while (workerState == STATE_STARTED) {
try {
startConsumer();
} catch (ShutdownSignalException sse) {
if (!sse.isInitiatedByApplication()) {
log.error("RabbitMQ Listener of the inbound " + inboundName +
" was disconnected", sse);
waitForConnection();
}
} catch (OMException e){
log.error("Invalid Message Format while consuming the message", e);
} catch (IOException e) {
log.error("RabbitMQ Listener of the inbound " + inboundName +
" was disconnected", e);
waitForConnection();
}
}
} catch (IOException e) {
handleException("Error initializing consumer for inbound " + inboundName, e);
} finally {
closeConnection();
workerState = STATE_STOPPED;
}
}
private void waitForConnection() throws IOException {
int retryInterval = rabbitMQConnectionFactory.getRetryInterval();
int retryCountMax = rabbitMQConnectionFactory.getRetryCount();
int retryCount = 0;
while ((workerState == STATE_STARTED) && !connection.isOpen()
&& ((retryCountMax == -1) || (retryCount < retryCountMax))) {
retryCount++;
log.info("Attempting to reconnect to RabbitMQ Broker for the inbound " + inboundName +
" in " + retryInterval + " ms");
try {
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
log.error("Error while trying to reconnect to RabbitMQ Broker for the inbound " +
inboundName, e);
}
}
if (connection.isOpen()) {
log.info("Successfully reconnected to RabbitMQ Broker for the inbound " + inboundName);
initConsumer();
} else {
log.error("Could not reconnect to the RabbitMQ Broker for the inbound " + inboundName +
". Connection is closed.");
workerState = STATE_FAULTY;
}
}
/**
* Used to start message consuming messages. This method is called in startup and when
* connection is re-connected. This method will request for the connection and create
* channel, queues, exchanges and bind queues to exchanges before consuming messages
*
* @throws ShutdownSignalException
* @throws IOException
*/
private void startConsumer() throws ShutdownSignalException, IOException {
connection = getConnection();
if (channel == null || !channel.isOpen()) {
channel = connection.createChannel();
log.debug("Channel is not open. Creating a new channel for inbound " + inboundName);
}
//set the qos value for the consumer//
String qos = rabbitMQProperties.getProperty(RabbitMQConstants.CONSUMER_QOS);
if (qos != null && !"".equals(qos)) {
channel.basicQos(Integer.parseInt(qos));
}
//unable to connect to the queue
if (queueingConsumer == null) {
workerState = STATE_STOPPED;
return;
}
while (isActive()) {
try {
if (!channel.isOpen()) {
channel = queueingConsumer.getChannel();
}
channel.txSelect();
} catch (IOException e) {
log.error("Error while starting transaction", e);
continue;
}
boolean successful = false;
RabbitMQMessage message = null;
try {
message = getConsumerDelivery(queueingConsumer);
} catch (InterruptedException e) {
log.error("Error while consuming message", e);
continue;
}
if (message != null) {
idle = false;
try {
successful = injectHandler.invoke(message, inboundName);
} finally {
if (successful) {
try {
if (!autoAck) {
channel.basicAck(message.getDeliveryTag(), false);
}
channel.txCommit();
} catch (IOException e) {
log.error("Error while committing transaction", e);
}
} else {
try {
channel.txRollback();
// According to the spec, rollback doesn't automatically redeliver unacked messages.
// We need to call recover explicitly.
channel.basicRecover();
} catch (IOException e) {
log.error("Error while trying to roll back transaction", e);
}
}
}
} else {
idle = true;
}
}
}
/**
* Create a queue consumer using the properties from inbound listener configuration
*
* @throws IOException on error
*/
private void initConsumer() throws IOException {
if (log.isDebugEnabled()) {
log.debug("Initializing consumer for inbound " + inboundName);
}
connection = getConnection();
channel = connection.createChannel();
queueName = rabbitMQProperties.getProperty(RabbitMQConstants.QUEUE_NAME);
routeKey = rabbitMQProperties.getProperty(RabbitMQConstants.QUEUE_ROUTING_KEY);
exchangeName = rabbitMQProperties.getProperty(RabbitMQConstants.EXCHANGE_NAME);
String autoAckStringValue = rabbitMQProperties.getProperty(RabbitMQConstants.QUEUE_AUTO_ACK);
if (autoAckStringValue != null) {
try {
autoAck = Boolean.parseBoolean(autoAckStringValue);
} catch (Exception e) {
log.debug("Format error in rabbitmq.queue.auto.ack parameter");
}
}
//If no queue name is specified then inbound factory name will be used as queue name
if (StringUtils.isEmpty(queueName)) {
queueName = inboundName;
log.info("No queue name is specified for " + inboundName + ". " +
"inbound factory name will be used as queue name");
}
if (routeKey == null) {
log.info("No routing key specified. Using queue name as the " + "routing key.");
routeKey = queueName;
}
if (!StringUtils.isEmpty(queueName)) {
//declaring queue
RabbitMQUtils.declareQueue(connection, queueName, rabbitMQProps);
}
if (!StringUtils.isEmpty(exchangeName)) {
//declaring exchange
RabbitMQUtils.declareExchange(connection, exchangeName, rabbitMQProps);
if (!channel.isOpen()) {
channel = connection.createChannel();
if (log.isDebugEnabled()) {
log.debug("Channel is not open. Creating a new channel for inbound " + inboundName);
}
}
channel.queueBind(queueName, exchangeName, routeKey);
log.debug("Bind queue '" + queueName + "' to exchange '" + exchangeName + "' with route key '" + routeKey + "'");
}
if (!channel.isOpen()) {
channel = connection.createChannel();
log.debug("Channel is not open. Creating a new channel for inbound " + inboundName);
}
queueingConsumer = new QueueingConsumer(channel);
consumerTagString = rabbitMQProperties.getProperty(RabbitMQConstants.CONSUMER_TAG);
if (consumerTagString != null) {
channel.basicConsume(queueName, autoAck, consumerTagString, queueingConsumer);
log.debug("Start consuming queue '" + queueName + "' with consumer tag '" + consumerTagString + "' for inbound " + inboundName);
} else {
consumerTagString = channel.basicConsume(queueName, autoAck, queueingConsumer);
log.debug("Start consuming queue '" + queueName + "' with consumer tag '" + consumerTagString + "' for inbound " + inboundName);
}
}
/**
* Returns the delivery from the consumer
*
* @param consumer the consumer to get the delivery
* @return RabbitMQMessage consumed by the consumer
* @throws InterruptedException on error
*/
private RabbitMQMessage getConsumerDelivery(QueueingConsumer consumer)
throws InterruptedException, ShutdownSignalException {
RabbitMQMessage message = new RabbitMQMessage();
QueueingConsumer.Delivery delivery = null;
try {
log.debug("Waiting for next delivery from queue for inbound " + inboundName);
delivery = consumer.nextDelivery();
} catch (ShutdownSignalException e) {
return null;
} catch (InterruptedException e) {
return null;
} catch (ConsumerCancelledException e) {
return null;
}
if (delivery != null) {
AMQP.BasicProperties properties = delivery.getProperties();
Map<String, Object> headers = properties.getHeaders();
message.setBody(delivery.getBody());
message.setDeliveryTag(delivery.getEnvelope().getDeliveryTag());
message.setReplyTo(properties.getReplyTo());
message.setMessageId(properties.getMessageId());
// Content type is as set in delivered message. If not, from inbound parameters.
String contentType = properties.getContentType();
if (contentType == null) {
contentType = rabbitMQProperties.getProperty(RabbitMQConstants.CONTENT_TYPE);
}
message.setContentType(contentType);
message.setContentEncoding(properties.getContentEncoding());
message.setCorrelationId(properties.getCorrelationId());
if (headers != null) {
message.setHeaders(headers);
if (headers.get(RabbitMQConstants.SOAP_ACTION) != null) {
message.setSoapAction(headers.get(
RabbitMQConstants.SOAP_ACTION).toString());
}
}
} else {
log.debug("Queue delivery item is null for inbound " + inboundName);
return null;
}
return message;
}
private void closeConnection() {
if (connection != null && connection.isOpen()) {
try {
connection.close();
log.info("RabbitMQ connection closed for inbound " + inboundName);
} catch (IOException e) {
log.error("Error while closing RabbitMQ connection for inbound " + inboundName, e);
} finally {
connection = null;
}
}
}
private Connection createConnection() throws IOException {
Connection connection = null;
try {
connection = rabbitMQConnectionFactory.createConnection();
log.info("RabbitMQ connection created for inbound " + inboundName);
} catch (Exception e) {
handleException("Error while creating RabbitMQ connection for inbound " + inboundName, e);
}
return connection;
}
private Connection getConnection() throws IOException {
if (connection == null) {
connection = createConnection();
setConnected(true);
}
return connection;
}
private boolean isActive() {
return workerState == STATE_STARTED;
}
public void setConnected(boolean connected) {
this.connected = connected;
}
public String getInboundName() {
return inboundName;
}
public void setInboundName(String inboundName) {
this.inboundName = inboundName;
}
protected void requestShutdown() {
workerState = STATE_SHUTTING_DOWN;
closeConnection();
}
private void handleException(String msg, Exception e) {
log.error(msg, e);
throw new RabbitMQException(msg, e);
}
}