de.tuberlin.cit.livescale.messaging.endpoints.AMQPEndpoint.java Source code

Java tutorial

Introduction

Here is the source code for de.tuberlin.cit.livescale.messaging.endpoints.AMQPEndpoint.java

Source

/***********************************************************************************************************************
 *
 * Copyright (C) 2010 by the Stratosphere project (http://stratosphere.eu)
 *
 * 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 de.tuberlin.cit.livescale.messaging.endpoints;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.ShutdownListener;
import com.rabbitmq.client.ShutdownSignalException;

import de.tuberlin.cit.livescale.messaging.MessageFactory;
import de.tuberlin.cit.livescale.messaging.MessageManifest;

/**
 * Represents an AMQPEndpoint.
 * 
 * Configuration keys:
 * <ul>
 * <li>{@link String} <code>brokerAddress</code> (required)</li>
 * <li>{@link String} <code>exchangeName</code> (required)</li>
 * <li>{@link Integer} <code>brokerPort</code> (optional)</li>
 * <li>{@link String} <code>routingKey</code> (optional, default "#")</li>
 * </ul>
 * 
 * A note on ReponseURIs. You'll need not provide a responseURI because frankly
 * you won't know what the "private" queue instanceName is.
 * ResponseURIs are provided by {@link AMQPEndpoint}
 * 
 * URI schema: amqp://[nameofthisinstance]/routingkey[?exchange=string]
 * 
 * @author Bernd Louis
 */
public class AMQPEndpoint extends AbstractMessageEndpoint {
    private static final String QUERY_KEY_EXCHANGE = "exchange";

    private static final Log LOG = LogFactory.getLog(AMQPEndpoint.class);

    protected static final String SCHEME = "amqp";

    // CONFIG KEY constants
    protected static final String CONFIG_KEY_ROUTING_KEY = "routingKey";
    protected static final String CONFIG_KEY_EXCHANGE_NAME = "exchangeName";
    protected static final String CONFIG_KEY_BROKER_PORT = "brokerPort";
    protected static final String CONFIG_KEY_BROKER_ADDRESS = "brokerAddress";
    protected static final String CONFIG_KEY_LISTEN_FOR_TASKS = "listenForTasks";
    protected static final String CONFIG_KEY_LISTEN_FOR_BROADCASTS = "listenForBroadcasts";
    private static final String[] requiredConfigFields = { CONFIG_KEY_BROKER_ADDRESS, CONFIG_KEY_EXCHANGE_NAME };

    // config options
    private String brokerAddress;
    private Integer brokerPort; // non primitive
    private String routingKey;
    private String exchangeName;

    private boolean listenForBroadcasts = false;
    private boolean listenForTasks = false;

    // AMQP stuff
    private static final String QUEUE_NAME_TASKS = "cit_stream_tasks_queue_";
    private static final String ROUTING_KEY_BROADCAST = "broadcast";
    private static final String ROUTING_KEY_TASK = "task";
    private static final String ROUTING_KEY_DELIMITER = ".";
    private static final String ROUTING_KEY_WILDCARD = "#";

    // "actual state"
    private ConnectionFactory connectionFactory = new ConnectionFactory();
    private Connection connection;
    private Channel channel;
    private String exclusiveQueueName;
    private QueueingConsumer consumer;
    private UUID uuid;

    /*
     * (non-Javadoc)
     * 
     * @see
     * de.tuberlin.cit.livescale.messaging.endpoints.MessageEndpoint#configure
     * (java.util.Map)
     */
    @Override
    public void configure(Map<String, String> properties) {
        this.assertRequiredConfigKeysPresent(properties);
        this.brokerAddress = properties.get(CONFIG_KEY_BROKER_ADDRESS);
        this.exchangeName = properties.get(CONFIG_KEY_EXCHANGE_NAME);
        try {
            this.brokerPort = Integer.parseInt(properties.get(CONFIG_KEY_BROKER_PORT));
        } catch (NumberFormatException e) {
            this.brokerPort = null;
            LOG.debug("Using default port");
        }
        this.routingKey = properties.get(CONFIG_KEY_ROUTING_KEY);
        if (this.routingKey == null) {
            this.routingKey = "#";
            LOG.debug(String.format("Using default routingKey", this.routingKey));
        }
        if (properties.containsKey(CONFIG_KEY_LISTEN_FOR_BROADCASTS)) {
            this.listenForBroadcasts = Boolean.valueOf(properties.get(CONFIG_KEY_LISTEN_FOR_BROADCASTS));
        }
        if (properties.containsKey(CONFIG_KEY_LISTEN_FOR_TASKS)) {
            this.listenForTasks = Boolean.valueOf(properties.get(CONFIG_KEY_LISTEN_FOR_TASKS));
        }
    }

    /**
     * Will throw {@link IllegalArgumentException} if not all required config
     * keys are present.
     * 
     * @param properties
     */
    private void assertRequiredConfigKeysPresent(Map<String, String> properties) {
        for (String k : requiredConfigFields) {
            if (!properties.containsKey(k)) {
                throw new IllegalArgumentException(String.format("Required configuration key missing: %s", k));
            }
        }
    }

    /* (non-Javadoc)
     * @see de.tuberlin.cit.livescale.messaging.endpoints.AbstractMessageEndpoint#performSend(de.tuberlin.cit.livescale.messaging.MessageManifest)
     */
    @Override
    protected void performSend(MessageManifest manifest) {
        URI targetURI = manifest.getTargetURI();
        // URI response = manifest.getResponseURI();
        BasicProperties bp = null;
        URIParser target = new URIParser(targetURI);

        // figure out our target
        String routingKey = target.getRoutingKey();
        String targetExchange = target.getExchangeName();
        // do we need a response? 
        if (manifest.isResponseRequired()) {
            bp = new BasicProperties.Builder().replyTo(this.exclusiveQueueName).appId(this.uuid.toString()).build();
        } else {
            bp = new BasicProperties.Builder().appId(this.uuid.toString()).build();
        }
        try {
            this.channel.basicPublish(targetExchange, routingKey, bp, MessageFactory.encode(manifest));
        } catch (IOException e) {
            notifyListenersDeliveryEvent(new ServiceUnavailableFailure(manifest));
        }
    }

    /**
     * Connects to the broker
     */
    private void connect() throws IOException {
        this.connectionFactory.setHost(this.brokerAddress);
        if (this.brokerPort != null) {
            this.connectionFactory.setPort(this.brokerPort);
        }
        this.connection = this.connectionFactory.newConnection();
        this.channel = this.connection.createChannel();
        this.channel.basicQos(1);
        // create the exchangeName 
        this.channel.exchangeDeclare(this.exchangeName, "topic", false, true, null);
        // my queue 
        this.exclusiveQueueName = this.channel.queueDeclare().getQueue();
        if (this.listenForBroadcasts) { // listen for "broadcast"
            this.channel.queueBind(this.exclusiveQueueName, this.exchangeName, ROUTING_KEY_BROADCAST);
        }
        // listen for "broadcast.[this.routingKey] e.g.
        // "broadcast.dispatcher" 
        this.channel.queueBind(this.exclusiveQueueName, this.exchangeName, new StringBuilder(ROUTING_KEY_BROADCAST)
                .append(ROUTING_KEY_DELIMITER).append(this.routingKey).toString());
        this.consumer = new QueueingConsumer(this.channel);
        LOG.info("Bound to " + new StringBuilder(ROUTING_KEY_BROADCAST).append(ROUTING_KEY_DELIMITER)
                .append(this.routingKey).toString());

        // declare a new queue with the instanceName "cit_stream_tasks_queue_[routingKey]"
        // for instance: "cit_stream_tasks_queue_dispatcher"
        // then bind this queue to this.exchangeName
        // "task.[routingKey]" e.g. "task.dispatcher"
        if (this.listenForTasks) {
            String queueNameTasks = new StringBuilder(QUEUE_NAME_TASKS).append(this.routingKey).toString();
            this.channel.queueDeclare(queueNameTasks, false, false, true, null);
            String listenRoutingKey = new StringBuilder(ROUTING_KEY_TASK).append(ROUTING_KEY_DELIMITER)
                    .append(this.routingKey).toString();
            this.channel.queueBind(queueNameTasks, this.exchangeName, listenRoutingKey);
        }
        this.uuid = UUID.randomUUID();
    }

    /**
     * Sets up a consumer that passes received messages to the
     * {@link MessageEndpointListener}s.
     */
    private void setUpConsumer() throws IOException {
        DefaultConsumer consumer = new DefaultConsumer(this.channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                long deliveryTag = envelope.getDeliveryTag();
                super.getChannel().basicAck(deliveryTag, false);
                if (!properties.getAppId().equals(AMQPEndpoint.this.uuid.toString())) {
                    MessageManifest mm = MessageFactory.decode(body);
                    try {
                        URI responseURI = new URIBuilder().routingKey(properties.getReplyTo())
                                .instanceName(getName()).build();
                        mm.setResponseURI(responseURI);
                    } catch (URISyntaxException e) { // this should never happen!
                        LOG.warn("Unable to create response URI for incoming message", e);
                    }
                    notifyMessageEndpointListeners(mm);
                }
            }
        };
        if (this.listenForTasks) {
            String queueName = QUEUE_NAME_TASKS + this.routingKey;
            this.channel.basicConsume(queueName, false, consumer);
        }
        this.channel.basicConsume(this.exclusiveQueueName, false, consumer);

        // listening to the remote shutdown
        ShutdownListener shutdownListener = new ShutdownListener() {
            @Override
            public void shutdownCompleted(ShutdownSignalException cause) {
                shutdown();
            }
        };
        this.channel.addShutdownListener(shutdownListener);
        //      if (this.channel.isOpen()) {
        //         handler.handleConnectionEvent(true);
        //      }
    }

    /* (non-Javadoc)
     * @see de.tuberlin.cit.livescale.messaging.endpoints.AbstractMessageEndpoint#getScheme()
     */
    @Override
    protected String getScheme() {
        return SCHEME;
    }

    /* (non-Javadoc)
     * @see de.tuberlin.cit.livescale.messaging.endpoints.MessageEndpoint#start()
     */
    @Override
    public void performStart() throws IOException {
        if (this.brokerAddress == null || this.routingKey == null) {
            throw new IllegalStateException(String.format("Cannot start %s, is not properly configured. ",
                    this.getClass().getSimpleName()));
        }
        connect();
        setUpConsumer();
    }

    /* (non-Javadoc)
     * @see de.tuberlin.cit.livescale.messaging.endpoints.MessageEndpoint#shutdown()
     */
    @Override
    public void performShutdown() {
        try {
            // do something with the consumer?
            if (this.channel != null && this.channel.isOpen()) {
                this.channel.close();
            }
            this.channel = null;
            if (this.connection != null && this.connection.isOpen()) {
                this.connection.close();
            }
            this.connection = null;
        } catch (IOException e) {
            LOG.warn("Errors while disconnecting from the broker.", e);
        }
    }

    /**
     * Builder class to create URIs compatible with an {@link AMQPEndpoint}
     * 
     * @author Bernd Louis
     */
    public static class URIBuilder {
        private String routingKey = "";
        private String instanceName = "";
        private String exchangeName = "";

        public URIBuilder routingKey(String routingKey) {
            this.routingKey = routingKey;
            return this;
        }

        public URIBuilder routingKey(String[] routingKey) {
            // this.routingKey =
            this.routingKey = routingKey[0];
            for (int i = 1; i < routingKey.length; i++) {
                this.routingKey += "." + routingKey[i];
            }
            return this;
        }

        public URIBuilder instanceName(String instanceName) {
            this.instanceName = instanceName;
            return this;
        }

        public URIBuilder exchangeName(String exchangeName) {
            this.exchangeName = exchangeName;
            return this;
        }

        public URI build() throws URISyntaxException {
            String path = "/" + this.routingKey;
            String query = "";
            String authority = this.instanceName;
            if (this.exchangeName != null && !(this.exchangeName.isEmpty())) {
                query = "exchange=" + this.exchangeName;
            }
            return new URI(AMQPEndpoint.SCHEME, authority, path, query, "");
        }

    }

    /**
     * This class is used to create a valid {@link Properties} instance.
     * 
     * {@link Properties} implements {@link Map<Object, Object>} 
     * 
     * @author Bernd Louis
     */
    public static class ConfigBuilder {
        private String routingKey = null;
        private String exchangeName = null;
        private int brokerPort = 0;
        private String brokerAddress = null;
        private boolean listenForTasks = false;
        private boolean listenForBroadcasts = false;

        public ConfigBuilder routingKey(String routingKey) {
            this.routingKey = routingKey;
            return this;
        }

        public ConfigBuilder exchangeName(String exchangeName) {
            this.exchangeName = exchangeName;
            return this;
        }

        public ConfigBuilder brokerPort(int brokerPort) {
            this.brokerPort = brokerPort;
            return this;
        }

        public ConfigBuilder brokerAddress(String brokerAddress) {
            this.brokerAddress = brokerAddress;
            return this;
        }

        public ConfigBuilder listenForTasks(boolean listenForTasks) {
            this.listenForTasks = listenForTasks;
            return this;
        }

        public ConfigBuilder listenForBroadcasts(boolean listenForBroadcasts) {
            this.listenForBroadcasts = listenForBroadcasts;
            return this;
        }

        public Properties build(String domain) {
            Properties map = new Properties();
            if (this.brokerAddress == null || this.exchangeName == null) {
                throw new IllegalArgumentException(
                        String.format("%s configuration is invalid", AMQPEndpoint.class.getSimpleName()));
            }
            String domainPrefix = "";
            if (domain != null && !(domain.isEmpty())) {
                domainPrefix = domain + ".";
            }
            if (this.routingKey != null) {
                map.put(domainPrefix + CONFIG_KEY_ROUTING_KEY, this.routingKey);
            }
            map.put(domainPrefix + "class", AMQPEndpoint.class.getName());
            map.put(domainPrefix + CONFIG_KEY_EXCHANGE_NAME, this.exchangeName);
            map.put(domainPrefix + CONFIG_KEY_BROKER_ADDRESS, this.brokerAddress);
            map.put(domainPrefix + CONFIG_KEY_BROKER_PORT, String.valueOf(this.brokerPort));
            map.put(domainPrefix + CONFIG_KEY_LISTEN_FOR_TASKS, String.valueOf(this.listenForTasks));
            map.put(domainPrefix + CONFIG_KEY_LISTEN_FOR_BROADCASTS, String.valueOf(this.listenForBroadcasts));
            return map;
        }

        public Properties build() {
            return build(null);
        }
    }

    /**
     * Helper class to parse the URI for the actual stuff
     * 
     * This is protected so we have some test on this
     * 
     * @author Bernd Louis
     */
    protected static class URIParser {
        private String routingKey = "";
        private String exchangeName = "";
        private String instanceName = null;
        private String scheme = null;

        /**
         * 
         * Initializes URIParser with a {@link URI}
         *
         * @param uri
         */
        public URIParser(URI uri) {
            this.scheme = uri.getScheme();
            this.routingKey = uri.getPath().substring(1); // skip the leading / 
            this.instanceName = uri.getAuthority();
            Map<String, String> query = getQuery(uri.getQuery());
            if (query != null && query.containsKey(QUERY_KEY_EXCHANGE)) {
                this.exchangeName = query.get(QUERY_KEY_EXCHANGE);
            }
        }

        /**
         * Parses a URI-Query-like string into a {@link Map}
         * 
         * @param query the Query-String
         * @return the parsed {@link Map}
         */
        private Map<String, String> getQuery(String query) {
            if (query == null)
                return null;
            Map<String, String> map = new HashMap<String, String>();
            String[] parts = query.split("&");
            for (String part : parts) {
                String[] keyValue = part.split("=");
                if (keyValue.length == 2) {
                    map.put(keyValue[0], keyValue[1]);
                }
            }
            return map;
        }

        /**
         * Returns the routingKey.
         * 
         * @return the routingKey
         */
        public String getRoutingKey() {
            return this.routingKey;
        }

        /**
         * Returns the exchangeName.
         * 
         * @return the exchangeName
         */
        public String getExchangeName() {
            return this.exchangeName;
        }

        /**
         * Returns the instanceName.
         * 
         * @return the instanceName
         */
        public String getInstanceName() {
            return this.instanceName;
        }

        /**
         * Returns the scheme.
         * 
         * @return the scheme
         */
        public String getScheme() {
            return this.scheme;
        }
    }
}