org.eclipse.flux.client.impl.RabbitMQMessageConnector.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.flux.client.impl.RabbitMQMessageConnector.java

Source

/*******************************************************************************
 * Copyright (c) 2014 Pivotal Software, Inc. and others.
 * All rights reserved. This program and the accompanying materials are made 
 * available under the terms of the Eclipse Public License v1.0 
 * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution 
 * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). 
 *
 * Contributors:
 *     Pivotal Software, Inc. - initial API and implementation
 *******************************************************************************/
package org.eclipse.flux.client.impl;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.HashSet;
import java.util.Set;

import org.eclipse.flux.client.FluxClient;
import org.eclipse.flux.client.MessageConstants;
import org.eclipse.flux.client.config.FluxConfig;
import org.eclipse.flux.client.config.RabbitMQFluxConfig;
import org.eclipse.flux.client.config.UserPermissions;
import org.eclipse.flux.client.util.Console;
import org.eclipse.flux.client.util.JSON;
import org.json.JSONObject;

import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.AMQP.Queue.DeclareOk;
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;

public class RabbitMQMessageConnector extends AbstractMessageConnector {

    //TODO: keep connectionSatus up-to-date when it get connected / disconnected / errors
    // Note: that this is not implemented is ok for now, as it is only used to report authentication
    // errors in the eclipse UI, which uses socketio connector.

    //Note: a channel in 'flux is not the same thing as a channel in AMQP.
    //  What is called a 'channel' in flux is really more like a
    //  'routing key' in AMQP.
    // A 'channel' in AMQP is more like a 'session object' used to interact
    // with the AMQP API.

    /**
     * Special user name routing key to deliver messages to all users.
     * This name is internal only, client code uses '*' in username.
     */
    private static final String EVERYONE = "$all$";

    private static Console console = Console.get(RabbitMQMessageConnector.class.getName());

    //private FluxClient client; // not used at the moment, so why store it?
    private RabbitMQFluxConfig conf;
    private ConnectionFactory factory;
    private UserPermissions permissions;

    private Connection connection;

    Channel channel;
    String inbox;
    String outbox;

    private DeliveryTypes deliveryTypes = DeliveryTypes.DEFAULTS;

    private Set<String> connectedChannels = new HashSet<String>();

    private ConnectionFactory connectionFactory() throws Exception {
        if (factory == null) {
            ConnectionFactory f = new ConnectionFactory();
            conf.applyTo(f);
            factory = f;
        }
        return factory;
    }

    public RabbitMQMessageConnector(FluxClient client, RabbitMQFluxConfig conf) throws Exception {
        super(client.getExecutor());
        this.conf = conf;
        this.connection = connectionFactory().newConnection();
        console.log("Connected to rabbitMQ: " + conf.getURI());
        this.permissions = conf.permissions();
        this.channel = connection.createChannel();
        this.inbox = createInbox();
        this.outbox = createOutbox();
        receiveBroadcasts();
    }

    /**
     * Subscribe to messages sent to EVERYONE.
     */
    private void receiveBroadcasts() throws IOException {
        this.channel.queueBind(inbox, outbox, EVERYONE);
        console.log("Connected to topic " + EVERYONE);
    }

    private String createInbox() throws IOException {
        DeclareOk ok = this.channel.queueDeclare("", /*durable*/ false, /*exclusive*/false, /*autoDelete*/true,
                null);
        final String inbox = ok.getQueue();
        console.log("Inbox created: " + inbox);
        channel.basicConsume(inbox, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                try {
                    JSONObject obj = JSON.parse(body);
                    if (!isSelfOriginated(obj)) {
                        handleIncomingMessage(obj.getString("type"), obj.getJSONObject("data"));
                    }
                } catch (Exception e) {
                    console.log(e);
                }
            }

            /**
             * Tests whether an incoming message originated from the same MessageConnector that
             * is receiving it. (Such messages are skipped in keeping with how socketio does the same
             * thing).
             */
            private boolean isSelfOriginated(JSONObject obj) {
                try {
                    String origin = obj.getString("origin");
                    return inbox.equals(origin);
                } catch (Exception e) {
                    console.log(e);
                }
                return false;
            }
        });
        return inbox;
    }

    private String createOutbox() throws IOException {
        String outbox = "flux"; //outbox queue/exchange name is the same 'flux' same for everyone.
        // rabbit mq routes messages placed in this queue via topic exchanges to
        // inboxes based on clients connectiong to a 'flux channels'.
        this.channel.exchangeDeclare(outbox, "topic");
        console.log("Outbox created");
        return outbox;
    }

    private static String channelNameToTopicPattern(String fluxChannelName) {
        checkValidChannel(fluxChannelName);
        if (fluxChannelName.equals(MessageConstants.SUPER_USER)) {
            return "*";
        }
        return fluxChannelName;
    }

    private static void checkValidChannel(String fluxChannelName) {
        //Check that channel name contains no chars that have special meaning in AMQP routing patterns.
        int len = fluxChannelName.length();
        for (int i = 0; i < len; i++) {
            char c = fluxChannelName.charAt(i);
            if (c == '.' || c == '#' || c == '*') {
                throw new IllegalArgumentException(
                        "Flux channel name '" + fluxChannelName + "' contains a special character '" + c + "'");
            }
        }
    }

    /**
     * Messages are routed based on the username in flux messages.
     */
    public String usernameToRoutingKey(String name) {
        if (name.equals("*")) {
            return EVERYONE;
        }
        return name;
    }

    @Override
    public void connectToChannel(final String channel) {
        executor.execute(new Runnable() {
            public void run() {
                try {
                    connectToChannelSync(channel);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    @Override
    public void connectToChannelSync(String channelName) throws Exception {
        boolean notifyNeeded = false;
        synchronized (this) {
            if (!isConnected(channelName)) {
                permissions.checkChannelJoin(channelName);
                String topic = channelNameToTopicPattern(channelName);
                this.channel.queueBind(this.inbox, this.outbox, topic);
                connectedChannels.add(channelName);
                notifyNeeded = true;
            }
        }
        if (notifyNeeded) {
            //Take care to call the listeners outside synch block to avoid potential deadlocks
            notifyChannelConnected(channelName);
        }
    }

    @Override
    public void disconnectFromChannelSync(String channelName) throws Exception {
        boolean notifyNeeded = false;
        synchronized (this) {
            if (isConnected(channelName)) {
                String topic = channelNameToTopicPattern(channelName);
                this.channel.queueUnbind(this.inbox, this.outbox, topic);
                connectedChannels.remove(channelName);
                notifyNeeded = true;
            }
        }
        if (notifyNeeded) {
            //Take care to call the listeners outside synch block to avoid potential deadlocks
            notifyChannelDisconnected(channelName);
        }
    }

    /**
     * Asynchronous version of disconnect from Channel. 
     */
    @Override
    public void disconnectFromChannel(final String channel) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    disconnectFromChannelSync(channel);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    @Override
    public synchronized boolean isConnected(String channel) {
        return connectedChannels.contains(channel);
    }

    @Override
    public void send(String messageType, JSONObject message) throws Exception {
        deliveryTypes.get(messageType).send(this, messageType, message);
    }

    @Override
    public synchronized void disconnect() {
        if (connection != null) {
            try {
                connection.close();
            } catch (IOException e) {
                console.log(e);
            }
            connection = null;
        }
    }

    @Override
    public synchronized boolean isConnected() {
        return connection != null && connection.isOpen();
    }

    @Override
    public FluxConfig getConfig() {
        return conf;
    }

    /**
     * Encode a message into byte array, ready to send over the wire.
     */
    byte[] encode(String messageType, JSONObject data) throws Exception {
        JSONObject message = new JSONObject().put("type", messageType).put("origin", inbox) //needed to avoid delivering broadcasts to 'self'
                .put("data", data);
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        OutputStreamWriter out = new OutputStreamWriter(bytes, "utf8");
        try {
            message.write(out);
        } finally {
            out.close();
        }
        return bytes.toByteArray();
    }

}