package dk.kb.yggdrasil.messaging; import java.io.IOException; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.rabbitmq.client.AMQP; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import com.rabbitmq.client.ConsumerCancelledException; import com.rabbitmq.client.QueueingConsumer; import com.rabbitmq.client.ShutdownSignalException; import dk.kb.yggdrasil.config.RabbitMqSettings; import dk.kb.yggdrasil.exceptions.ArgumentCheck; import dk.kb.yggdrasil.exceptions.RabbitException; import dk.kb.yggdrasil.exceptions.YggdrasilException; import dk.kb.yggdrasil.json.JSONMessaging; import dk.kb.yggdrasil.json.preservation.PreservationResponse; import dk.kb.yggdrasil.json.preservationimport.PreservationImportResponse; /** * Methods for publishing messages on a queue and receiving from a queue * using an RabbitMQ broker. * Tested with RabbitMQ broker 3.1.5, and amqp-client 3.1.4 (3.1.5 is * not available on maven repositories). * rabbitmq-javadoc: http://www.rabbitmq.com/releases/rabbitmq-java-client/v3.1.4/rabbitmq-java-client-javadoc-3.1.4/ */ public class MQ implements AutoCloseable { /** List of existing consumers in use by this class. * The key is the queueName. */ protected Map<String, QueueingConsumer> existingConsumers; /** List of existing consumers in use by this class identified by consumertags. */ protected Set<String> existingConsumerTags; /** channel to the broker. Is one channel enough? */ protected Channel theChannel; /** The settings used to create the broker configurations. */ protected RabbitMqSettings settings; /** Default exchangename to be used by all queues. */ protected String exchangeName = "exchange"; //TODO should this be a parameter in the settings? /** exchange type direct means a message sent to only one recipient. */ protected String exchangeType = "direct"; /** The message type for initiating the preservation. */ public static final String PRESERVATIONREQUEST_MESSAGE_TYPE = "PreservationRequest"; /** The message type for responding to preservation requests. */ public static final String PRESERVATIONRESPONSE_MESSAGE_TYPE = "PreservationResponse"; /** The message type for request for importing preserved data. */ public static final String IMPORTREQUEST_MESSAGE_TYPE = "PreservationImportRequest"; /** The message type for responding to import requests. */ public static final String IMPORTRESPONSE_MESSAGE_TYPE = "PreservationImportResponse"; /** The only valid message type, currently. */ public static final String SHUTDOWN_MESSAGE_TYPE = "Shutdown"; /** Logging mechanism. */ private static Logger logger = LoggerFactory.getLogger(MQ.class.getName()); /** * Constructor for the MQ object. * @param settings The settings used to create the broker connection. * @throws YggdrasilException If it fails. * @throws RabbitException When message queue connection fails. */ public MQ(RabbitMqSettings settings) throws YggdrasilException, RabbitException { this.existingConsumerTags = new HashSet<String>(); this.existingConsumers = new HashMap<String, QueueingConsumer>(); this.settings = settings; ConnectionFactory factory = new ConnectionFactory(); Connection conn = null; try { factory.setUri(settings.getBrokerUri()); conn = factory.newConnection(); logger.info("Connecting to RabbitMQ on server: " + conn.getAddress().getCanonicalHostName()); theChannel = conn.createChannel(); configureChannel(settings.getPreservationDestination()); configureChannel(settings.getPreservationResponseDestination()); configureChannel(settings.getShutdownDestination()); } catch (KeyManagementException e1) { throw new YggdrasilException("Error connecting to Broker at '" + settings.getBrokerUri() + "' : ", e1); } catch (NoSuchAlgorithmException e2) { throw new YggdrasilException("Error connecting to Broker at '" + settings.getBrokerUri() + "' : ", e2); } catch (URISyntaxException e3) { throw new YggdrasilException("Error connecting to Broker at '" + settings.getBrokerUri() + "' : ", e3); } catch (IOException e4) { throw new RabbitException("Error connecting to Broker at '" + settings.getBrokerUri() + "' : ", e4); } } /** * Close channel to broker, and cancel the associated consumers. * @throws IOException If it fails to close the connection. */ public void close() throws IOException { if (theChannel != null && theChannel.isOpen()) { // close existing consumers before closing the channel its connection. for (String tag: existingConsumerTags) { theChannel.basicCancel(tag); } Connection conn = theChannel.getConnection(); theChannel.close(); conn.close(); } } /** * @return a set of AMQP properties for */ public static AMQP.BasicProperties getMQProperties() { AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder(); AMQP.BasicProperties persistentTextXml = builder.deliveryMode(2).contentType("text/json").build(); return persistentTextXml; } /** * Configure a channel with the default configuration. * @param destination The destination to configure, * @throws YggdrasilException When problem with configuring the channel. */ protected void configureChannel(String destination) throws YggdrasilException { try { String queueName = destination; String routingKey = destination; // These next 3 lines are not necessarily all needed boolean durableExchange = true; // Exchanges will survive a rabbitmq server crash. theChannel.exchangeDeclare(exchangeName, exchangeType, durableExchange); boolean durableQueue = true; boolean exclusive = false; // meaning restricted to this connection boolean autodelete = false; //meaning delete when no longer used Map<String, Object> arguments = null; theChannel.queueDeclare(queueName, durableQueue, exclusive, autodelete, arguments); // Bind a queue to a given exchange theChannel.queueBind(queueName, exchangeName, routingKey); } catch (IOException e) { throw new YggdrasilException("Problems configuring the broker", e); } } /** * Publish a message on the given queue. * @param queueName A given MQ queue. * @param message The message to be published on the queue. * @param messageType The Type of the message * @throws YggdrasilException If Unable to publish message to the queue. */ public void publishOnQueue(String queueName, byte[] message, String messageType) throws YggdrasilException { try { String routingKey = queueName; AMQP.BasicProperties messageProps = MQ.getMQProperties(); messageProps.setType(messageType); messageProps.setTimestamp(new Date()); logger.debug("Publishing message on a queue: {} at {}\n {}", queueName, settings.getBrokerUri(), new String(message, Charset.defaultCharset())); theChannel.basicPublish(exchangeName, routingKey, messageProps, message); } catch (IOException e) { throw new YggdrasilException("Unable to publish message to queue '" + queueName + "'", e); } } /** * Receive message from a given queue. If no message is waiting on the queue, this message will * wait until a message arrives on the queue. * @param queueName The name of the queue. * @return the messageType and bytes delivered in the message when a message is received. * @throws YggdrasilException If it fails. * @throws RabbitException When message queue connection fails. */ public MqResponse receiveMessageFromQueue(String queueName) throws YggdrasilException, RabbitException { ArgumentCheck.checkNotNullOrEmpty(queueName, "String queueName"); QueueingConsumer consumer = null; String consumerTag = null; if (existingConsumers.containsKey(queueName)) { consumer = existingConsumers.get(queueName); } else { consumer = new QueueingConsumer(theChannel); try { consumerTag = theChannel.basicConsume(queueName, consumer); existingConsumers.put(queueName, consumer); existingConsumerTags.add(consumerTag); } catch (IOException e) { throw new YggdrasilException("Unable to attach to queue '" + queueName + "'", e); } } byte[] payload = null; String messageType = null; try { QueueingConsumer.Delivery delivery = consumer.nextDelivery(); messageType = delivery.getProperties().getType(); Date sentDate = delivery.getProperties().getTimestamp(); logger.info("received message of type '" + messageType + "' with timestamp '" + sentDate + "'"); payload = delivery.getBody(); boolean acknowledgeMultipleMessages = false; theChannel.basicAck(delivery.getEnvelope().getDeliveryTag(), acknowledgeMultipleMessages); } catch (IOException e) { throw new YggdrasilException("Unable to receive message from queue '" + queueName + "'", e); } catch (ShutdownSignalException e) { throw new RabbitException("Unable to receive message from queue '" + queueName + "'", e); } catch (ConsumerCancelledException e) { throw new YggdrasilException("Unable to receive message from queue '" + queueName + "'", e); } catch (InterruptedException e) { throw new YggdrasilException("Unable to receive message from queue '" + queueName + "'", e); } return new MqResponse(messageType, payload); } /** * Purges the queue. * @param queue The queue to purge. * @throws RabbitException */ public void purgeQueue(String queue) throws RabbitException { try { theChannel.queuePurge(queue); } catch (IOException e) { throw new RabbitException("Could not purge the queue.", e); } } /** * Publishes a preservation response message. * @param response The preservation response message. * @throws YggdrasilException If unable to publish the preservation response on the message queue. */ public void publishPreservationResponse(PreservationResponse response) throws YggdrasilException { byte[] responseBytes = JSONMessaging.getPreservationResponse(response); publishOnQueue(settings.getPreservationResponseDestination(), responseBytes, MQ.PRESERVATIONRESPONSE_MESSAGE_TYPE); } /** * Publishes a preservation response message. * @param response The preservation response message. * @throws YggdrasilException If unable to publish the preservation response on the message queue. */ public void publishPreservationImportResponse(PreservationImportResponse response) throws YggdrasilException { byte[] responseBytes = JSONMessaging.getPreservationImportResponse(response); publishOnQueue(settings.getPreservationResponseDestination(), responseBytes, MQ.IMPORTRESPONSE_MESSAGE_TYPE); } /** * @return The settings. */ public RabbitMqSettings getSettings() { return this.settings; } }