Commits

Chris Thunes committed 0f03a64

Lot of refactoring to make the client code less coupled to the
underlying connection management. Also remove IRC prefix from
everything.

Comments (0)

Files changed (73)

brewtab-irc/pom.xml

       <artifactId>junit</artifactId>
       <scope>test</scope>
     </dependency>
+
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 </project>

brewtab-irc/src/main/java/com/brewtab/irc/Connection.java

+package com.brewtab.irc;
+
+import java.util.List;
+
+import org.jboss.netty.channel.ChannelFuture;
+
+import com.brewtab.irc.messages.Message;
+import com.brewtab.irc.messages.MessageListener;
+import com.brewtab.irc.messages.filter.MessageFilter;
+
+/**
+ * Represents a general IRC connection. The connection may be client to server,
+ * server to client, or server to server.
+ */
+public interface Connection {
+    /**
+     * Add a message listener to the connection. If this listener has had
+     * already been registered with a different filter, its filter will be
+     * replaced with the one provided.
+     * 
+     * @param filter
+     * @param listener
+     */
+    public void addMessageListener(MessageFilter filter, MessageListener listener);
+
+    /**
+     * Remove a previously added message listener.
+     * 
+     * @param listener
+     */
+    public void removeMessageListener(MessageListener listener);
+
+    /**
+     * Add a connection state listener.
+     * 
+     * @param listener
+     */
+    public void addConnectionStateListener(ConnectionStateListener listener);
+
+    /**
+     * Remove a connection state listener.
+     * 
+     * @param listener
+     */
+    public void removeConnectionStateListener(ConnectionStateListener listener);
+
+    /**
+     * Send a message on this connection.
+     * 
+     * @param message
+     * @return A Netty {@link ChannelFuture} object for the send operation
+     */
+    public ChannelFuture send(Message message);
+
+    /**
+     * Send a multiple messages on this connection.
+     * 
+     * @param message
+     * @return A Netty {@link ChannelFuture} object for the send operation
+     */
+    public ChannelFuture send(Message... messages);
+
+    /**
+     * Send a message and return a response.
+     * 
+     * @param match a filter to match messages which should be included in the response
+     * @param last a filter to match the last message of the response
+     * @param message the message to send
+     * @return a list of response messages matching the provided filters
+     * @throws InterruptedException
+     */
+    public List<Message> request(MessageFilter match, MessageFilter last, Message message) throws InterruptedException;
+
+    /**
+     * Send a message and return a response.
+     * 
+     * @param match a filter to match messages which should be included in the response
+     * @param last a filter to match the last message of the response
+     * @param message the message to send
+     * @return a list of response messages matching the provided filters
+     * @throws InterruptedException
+     */
+    public List<Message> request(MessageFilter match, MessageFilter last, Message... messages) throws InterruptedException;
+
+    /**
+     * Close this connection.
+     * 
+     * @return A Netty {@link ChannelFuture} for the close operation
+     */
+    public ChannelFuture close();
+
+    /**
+     * Check if the connection is connected
+     * 
+     * @return
+     */
+    public boolean isConnected();
+}

brewtab-irc/src/main/java/com/brewtab/irc/ConnectionException.java

+package com.brewtab.irc;
+
+public class ConnectionException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    public ConnectionException(Throwable e) {
+        super(e);
+    }
+
+    public ConnectionException(String message) {
+        super(message);
+    }
+}

brewtab-irc/src/main/java/com/brewtab/irc/ConnectionStateListener.java

+package com.brewtab.irc;
+
+public interface ConnectionStateListener {
+    /**
+     * Called when a connection is connected.
+     */
+    public void onConnectionConnected();
+
+    /**
+     * Called when a connection has started being closed.
+     */
+    public void onConnectionClosing();
+
+    /**
+     * Called when a connection has been closed.
+     */
+    public void onConnectionClosed();
+}

brewtab-irc/src/main/java/com/brewtab/irc/IRCChannel.java

-package com.brewtab.irc;
-
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.concurrent.CountDownLatch;
-
-import com.brewtab.irc.messages.IRCMessage;
-import com.brewtab.irc.messages.IRCMessageType;
-import com.brewtab.irc.messages.filter.IRCMessageFilter;
-import com.brewtab.irc.messages.filter.IRCMessageFilters;
-import com.brewtab.irc.messages.filter.IRCMessageOrFilter;
-
-/**
- * An IRC channel. Represents a connection to an IRC channel.
- * 
- * @author Christopher Thunes <cthunes@brewtab.com>
- */
-public class IRCChannel implements IRCMessageHandler {
-    /* Channel name */
-    private String name;
-
-    /* Associated IRCClient object */
-    private IRCClient client;
-
-    /* Set once the channel has been joined */
-    private CountDownLatch joined;
-
-    /* IRCChanneListners for this channel */
-    private LinkedList<IRCChannelListener> listeners;
-
-    /* List of nicks in the channel */
-    private ArrayList<String> names;
-
-    /**
-     * Message handler that receives responses from NAMES requests for this
-     * channel and creates a list of names. NamesRequestHandler#getNames can be
-     * called to wait for the list to be fully populated at which time all names
-     * are returned as an array.
-     * 
-     * @author Christopher Thunes <cthunes@brewtab.com>
-     */
-    private class NamesRequestHandler implements IRCMessageHandler, IRCMessageFilter {
-        private ArrayList<String> names;
-        private CountDownLatch done;
-
-        /**
-         * Create a new NamesRequestHandler
-         */
-        public NamesRequestHandler() {
-            this.names = new ArrayList<String>();
-            this.done = new CountDownLatch(1);
-        }
-
-        @Override
-        public void handleMessage(IRCMessage message) {
-            switch (message.getType()) {
-            case RPL_NAMREPLY:
-                String[] args = message.getArgs();
-                for (String name : args[args.length - 1].split(" ")) {
-                    names.add(name.replaceFirst("^[@+]", ""));
-                }
-                break;
-
-            case RPL_ENDOFNAMES:
-                this.done.countDown();
-                break;
-
-            default:
-                break;
-            }
-        }
-
-        @Override
-        public boolean check(IRCMessage message) {
-            String channelName = IRCChannel.this.getName();
-            String[] args = message.getArgs();
-
-            switch (message.getType()) {
-            case RPL_NAMREPLY:
-                if (args.length > 2 && args[2].equals(channelName)) {
-                    return true;
-                }
-                break;
-
-            case RPL_ENDOFNAMES:
-                if (args.length > 1 && args[1].equals(channelName)) {
-                    return true;
-                }
-                break;
-
-            default:
-                break;
-            }
-
-            return false;
-        }
-
-        /**
-         * Wait for a full request to be replied to and then return the list of
-         * names
-         * 
-         * @return the list of names in the channel
-         */
-        public ArrayList<String> getNames() {
-            try {
-                this.done.await();
-            } catch (InterruptedException e) {
-                // TODO: Log and return whatever we have
-            }
-
-            return this.names;
-        }
-    }
-
-    /**
-     * Instantiate a new IRCChannel object associated with the given IRCClient
-     * 
-     * @param client
-     *            The client to operate with
-     * @param name
-     *            The channel to join
-     */
-    public IRCChannel(IRCClient client, String name) {
-        this.client = client;
-        this.name = name;
-        this.joined = new CountDownLatch(1);
-        this.listeners = new LinkedList<IRCChannelListener>();
-        this.names = new ArrayList<String>();
-
-        /*
-         * Build a compound filter collecting messages sent regarding this
-         * channel and include all QUIT messages. Some QUIT messages will
-         * concern this channel if it's a user in this channel QUITing
-         */
-        IRCMessageFilter channelFilter = IRCMessageFilters.newChannelFilter(name);
-        IRCMessageFilter quitFilter = IRCMessageFilters.newTypeFilter(IRCMessageType.QUIT);
-        IRCMessageFilter nickFilter = IRCMessageFilters.newTypeFilter(IRCMessageType.NICK);
-        IRCMessageFilter filter = new IRCMessageOrFilter(channelFilter, quitFilter, nickFilter);
-
-        this.client.addHandler(filter, this);
-    }
-
-    /**
-     * Get the channel name
-     * 
-     * @return the channel name
-     */
-    public String getName() {
-        return this.name;
-    }
-
-    /**
-     * Get the associated IRCClient
-     * 
-     * @return the associated IRCClient
-     */
-    public IRCClient getClient() {
-        return this.client;
-    }
-
-    /**
-     * Perform the join to the channel. Returns once the join operation is
-     * complete
-     * 
-     * @return true if joined successfully, false otherwise
-     */
-    public boolean doJoin() {
-        IRCMessage joinMessage = new IRCMessage(IRCMessageType.JOIN, this.name);
-        NamesRequestHandler namesHandler = new NamesRequestHandler();
-
-        this.client.addHandler(namesHandler, namesHandler);
-        this.client.getConnection().sendMessage(joinMessage);
-
-        try {
-            this.joined.await();
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            return false;
-        }
-
-        this.names = namesHandler.getNames();
-        this.client.removeHandler(namesHandler);
-
-        return true;
-    }
-
-    /**
-     * Part the channel
-     * 
-     * @param reason
-     *            The reason sent with the PART message
-     */
-    public void part(String reason) {
-        IRCMessage partMessage = new IRCMessage(IRCMessageType.PART, this.name, reason);
-        this.client.getConnection().sendMessage(partMessage);
-        this.joined = new CountDownLatch(1);
-    }
-
-    /**
-     * Write a message to the channel
-     * 
-     * @param text
-     *            The message to send
-     */
-    public void write(String text) {
-        IRCMessage privMessage = new IRCMessage(IRCMessageType.PRIVMSG, this.name, text);
-        this.client.getConnection().sendMessage(privMessage);
-    }
-
-    /**
-     * Write multiple messages efficiently
-     * 
-     * @param strings
-     *            The messages to send
-     */
-    public void writeMultiple(String... strings) {
-        IRCMessage[] privMessages = new IRCMessage[strings.length];
-        for (int i = 0; i < strings.length; i++) {
-            privMessages[i] = new IRCMessage(IRCMessageType.PRIVMSG, this.name, strings[i]);
-        }
-        this.client.getConnection().sendMessages(privMessages);
-    }
-
-    /**
-     * Retrieve the list of users in the channel. This call returns a cached
-     * copy. To request a refresh of the listing from the server call
-     * IRChannel#refreshNames.
-     * 
-     * @return the list of nicks of users in the channel
-     */
-    public String[] getNames() {
-        String[] tempNames = new String[this.names.size()];
-        tempNames = this.names.toArray(tempNames);
-        return tempNames;
-    }
-
-    /**
-     * Makes a NAMES request to the server for this channel. Store the result
-     * replacing any existing names list. The list can be retrieved with
-     * IRCChannel#getNames
-     */
-    public void refreshNames() {
-        IRCMessage message = new IRCMessage(IRCMessageType.NAMES, this.name);
-        NamesRequestHandler handler = new NamesRequestHandler();
-
-        this.client.addHandler(handler, handler);
-        this.client.getConnection().sendMessage(message);
-        this.names = handler.getNames();
-        this.client.removeHandler(handler);
-    }
-
-    /**
-     * Add an IRCChannelListener to this channel.
-     * 
-     * @param listener
-     *            the listener to add
-     */
-    public void addListener(IRCChannelListener listener) {
-        this.listeners.add(listener);
-    }
-
-    @Override
-    public void handleMessage(IRCMessage message) {
-        IRCUser user = IRCUser.fromString(message.getPrefix());
-
-        switch (message.getType()) {
-        case JOIN:
-            /* Must have valid user prefix */
-            if (user == null) {
-                break;
-            }
-
-            /*
-             * Once we've received a join message from the server we ourselves
-             * have actually joined
-             */
-            this.joined.countDown();
-
-            /* Add user to names list */
-            if (!this.names.contains(user.getNick())) {
-                this.names.add(user.getNick());
-            }
-
-            /* Call listeners */
-            for (IRCChannelListener listener : this.listeners) {
-                listener.onJoin(this, user);
-            }
-            break;
-
-        case PART:
-            /* Must have valid user prefix */
-            if (user == null) {
-                break;
-            }
-
-            /* Remove nick from names list */
-            if (this.names.contains(user.getNick())) {
-                this.names.remove(user.getNick());
-            }
-
-            /* Call listeners */
-            for (IRCChannelListener listener : this.listeners) {
-                listener.onPart(this, user);
-            }
-            break;
-
-        case PRIVMSG:
-            /* Must have valid user prefix */
-            if (user == null) {
-                break;
-            }
-
-            /* Call listeners */
-            for (IRCChannelListener listener : this.listeners) {
-                listener.onPrivateMessage(this, message.getArgs()[1], user);
-            }
-            break;
-
-        case QUIT:
-            /* Must have valid user prefix */
-            if (user == null) {
-                break;
-            }
-
-            /* Remove nick from names list */
-            if (this.names.contains(user.getNick())) {
-                this.names.remove(user.getNick());
-            }
-
-            /* Call listeners */
-            for (IRCChannelListener listener : this.listeners) {
-                listener.onQuit(this, user);
-            }
-            break;
-
-        case NICK:
-            /* Must have valid user prefix */
-            if (user == null) {
-                break;
-            }
-
-            /* Replace nick in names list */
-            if (this.names.contains(user.getNick())) {
-                this.names.remove(user.getNick());
-                this.names.add(message.getArgs()[0]);
-            }
-            break;
-
-        default:
-            break;
-        }
-    }
-}

brewtab-irc/src/main/java/com/brewtab/irc/IRCChannelListener.java

-package com.brewtab.irc;
-
-/**
- * Implementing class can listen for IRC channel related events
- * 
- * @author Christopher Thunes <cthunes@brewtab.com>
- */
-public interface IRCChannelListener {
-    /**
-     * Called when a user joins the channel
-     * 
-     * @param channel
-     *            The channel that was joined
-     * @param user
-     *            The user that joined
-     */
-    public void onJoin(IRCChannel channel, IRCUser user);
-
-    /**
-     * Called when a user parts the channel
-     * 
-     * @param channel
-     *            The channel that was parted from
-     * @param user
-     *            The user that parted
-     */
-    public void onPart(IRCChannel channel, IRCUser user);
-
-    /**
-     * Called when a user quits the channel
-     * 
-     * @param channel
-     *            The channel that was quit from
-     * @param user
-     *            The user that parted
-     */
-    public void onQuit(IRCChannel channel, IRCUser user);
-
-    /**
-     * Called whenever a message is sent to the channel
-     * 
-     * @param channel
-     *            The channel the message was received on
-     * @param message
-     *            The message
-     * @param from
-     *            The user that sent the message
-     */
-    public void onPrivateMessage(IRCChannel channel, String message, IRCUser from);
-}

brewtab-irc/src/main/java/com/brewtab/irc/IRCClient.java

-package com.brewtab.irc;
-
-import java.net.InetSocketAddress;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-import org.jboss.netty.bootstrap.ClientBootstrap;
-import org.jboss.netty.channel.ChannelFactory;
-import org.jboss.netty.channel.ChannelFuture;
-import org.jboss.netty.channel.ChannelFutureListener;
-import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.brewtab.irc.messages.IRCMessage;
-import com.brewtab.irc.messages.IRCMessageType;
-import com.brewtab.irc.messages.filter.IRCMessageFilter;
-import com.brewtab.irc.protocol.IRCChannelHandler;
-import com.brewtab.irc.protocol.IRCChannelPipelineFactory;
-
-public class IRCClient implements IRCConnectionManager {
-    private static final Logger log = LoggerFactory.getLogger(IRCClient.class);
-
-    /* Netty objects */
-    private ChannelFactory channelFactory;
-    private ClientBootstrap bootstrap;
-    private IRCChannelHandler channelHandler;
-    private IRCChannelPipelineFactory pipelineFactory;
-
-    /* The address of the server */
-    private InetSocketAddress address;
-
-    /* Cached copy of the server's name */
-    private String servername;
-
-    /* Connection information */
-    private String pass;
-    private String nick;
-    private String username;
-    private String hostname;
-    private String realname;
-
-    /* User object, constructed from nick, username, and hostname */
-    private IRCUser user;
-
-    /* Designate connection status */
-    private CountDownLatch connected;
-    private boolean connectedSuccessfully;
-
-    /* Thread pool from which message handlers are dispatched */
-    private ExecutorService threadPool;
-
-    /* Filter -> Handler mapping */
-    private HashMap<IRCMessageFilter, IRCMessageHandler> handlers;
-    private HashSet<IRCMessageFilter> handlersKeys;
-
-    /**
-     * Wraps a message handler so it can be passed to the thread pool
-     * 
-     * @author Christopher Thunes <cthunes@brewtab.com>
-     */
-    private class MessageHandlerRunnable implements Runnable {
-        private IRCMessageHandler handler;
-        private IRCMessage message;
-
-        /**
-         * Create a new wrapper for the given handler and the given message
-         * 
-         * @param handler
-         *            The handler to call
-         * @param message
-         *            The message to pass
-         */
-        public MessageHandlerRunnable(IRCMessageHandler handler, IRCMessage message) {
-            this.handler = handler;
-            this.message = message;
-        }
-
-        @Override
-        public void run() {
-            try {
-                this.handler.handleMessage(this.message);
-            } catch (Exception e) {
-                log.error("Caught exception from message handler", e);
-            }
-        }
-    }
-
-    /**
-     * Construct a new IRCClient connected to the given address
-     * 
-     * @param address
-     *            The address to connect to
-     */
-    public IRCClient(InetSocketAddress address) {
-        this.channelFactory = new NioClientSocketChannelFactory(
-            Executors.newCachedThreadPool(),
-            Executors.newCachedThreadPool());
-
-        this.bootstrap = new ClientBootstrap(this.channelFactory);
-        this.channelHandler = null;
-        this.pipelineFactory = new IRCChannelPipelineFactory(this);
-
-        this.bootstrap.setPipelineFactory(this.pipelineFactory);
-        this.bootstrap.setOption("tcpNoDelay", true);
-
-        this.address = address;
-        this.servername = this.address.getHostName();
-
-        this.connected = new CountDownLatch(1);
-        this.connectedSuccessfully = false;
-
-        this.handlers = new HashMap<IRCMessageFilter, IRCMessageHandler>();
-        this.handlersKeys = new HashSet<IRCMessageFilter>();
-
-        this.threadPool = Executors.newCachedThreadPool();
-
-        this.pass = null;
-        this.nick = null;
-        this.username = null;
-        this.hostname = null;
-        this.realname = null;
-
-        this.user = null;
-    }
-
-    /**
-     * Get the user information give by the IRCClient#connect method as an
-     * IRCUser object
-     * 
-     * @return the IRCUser object or null if the client is not connected yet
-     */
-    public IRCUser getUser() {
-        return this.user;
-    }
-
-    /**
-     * Connect without a password and with the given information
-     * 
-     * @param nick
-     *            The nick to connection with
-     * @param username
-     *            The user name to connect with
-     * @param hostname
-     *            The host name to connect with
-     * @param realname
-     *            The real name to connect with
-     * @return true if the connection was successfully made, false otherwise
-     */
-    public boolean connect(String nick, String username, String hostname, String realname) {
-        return this.connect(null, nick, username, hostname, realname);
-    }
-
-    /**
-     * Connect with a password and with the given information
-     * 
-     * @param pass
-     *            The password to authenticate with
-     * @param nick
-     *            The nick to connection with
-     * @param username
-     *            The user name to connect with
-     * @param hostname
-     *            The host name to connect with
-     * @param realname
-     *            The real name to connect with
-     * @return true if the connection was successfully made, false otherwise
-     */
-    public boolean connect(String pass, String nick, String username, String hostname, String realname) {
-        /* Save connection information */
-        this.pass = pass;
-        this.nick = nick;
-        this.username = username;
-        this.hostname = hostname;
-        this.realname = realname;
-
-        this.user = new IRCUser(nick, username, hostname);
-
-        /* Perform connection */
-        ChannelFuture future = this.bootstrap.connect(this.address);
-
-        /* Wait for underly socket connection to be made */
-        future.awaitUninterruptibly();
-        if (!future.isSuccess()) {
-            /* Could not connection to remote host */
-            return false;
-        }
-
-        /* Now wait for IRC connection */
-        try {
-            this.connected.await();
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            return false;
-        }
-
-        return this.connectedSuccessfully;
-    }
-
-    /**
-     * Quit from the server and close all network connections
-     * 
-     * @param reason
-     *            The reason for quitting as given in the QUIT message
-     */
-    public void quit(String reason) {
-        IRCMessage quit = new IRCMessage(IRCMessageType.QUIT, reason);
-        ChannelFuture future = this.channelHandler.sendMessage(quit);
-
-        /* Wait for message to be sent and then close the underlying connection */
-        future.addListener(new ChannelFutureListener() {
-            @Override
-            public void operationComplete(ChannelFuture future) throws Exception {
-                IRCClient.this.channelHandler.closeChannel();
-            }
-        });
-
-    }
-
-    /**
-     * Join the given channel. Create a new channel object for this connection,
-     * join the channel and return the IRCChannel object.
-     * 
-     * @param channelName
-     *            The channel to join
-     * @return the new IRCChannel object or null if the channel could not be
-     *         joined
-     */
-    public IRCChannel join(String channelName) {
-        IRCChannel channel = new IRCChannel(this, channelName);
-
-        /* Attempt to join */
-        if (channel.doJoin()) {
-            return channel;
-        }
-
-        /* Error while joining */
-        return null;
-    }
-
-    public IRCPrivateChat getPrivateChat(String nick) {
-        return new IRCPrivateChat(this, nick);
-    }
-
-    @Override
-    public void onConnect(IRCChannelHandler connection) {
-        this.channelHandler = connection;
-
-        IRCMessage nickMessage = new IRCMessage(IRCMessageType.NICK, this.nick);
-        IRCMessage userMessage = new IRCMessage(IRCMessageType.USER, this.username, this.hostname, this.servername,
-                this.realname);
-
-        if (this.pass != null) {
-            /* Send a PASS message first */
-            IRCMessage passMessage = null;
-            if (this.pass != null) {
-                passMessage = new IRCMessage(IRCMessageType.PASS, this.pass);
-            }
-
-            connection.sendMessages(passMessage, nickMessage, userMessage);
-        } else {
-            connection.sendMessages(nickMessage, userMessage);
-        }
-    }
-
-    /**
-     * Add a message handler for this connection. Messages matching the given
-     * filter are passed onto the given handler
-     * 
-     * @param filter
-     *            The filter to match messages with
-     * @param handler
-     *            The handler to call with matching message
-     */
-    public void addHandler(IRCMessageFilter filter, IRCMessageHandler handler) {
-        synchronized (this.handlers) {
-            this.handlers.put(filter, handler);
-            this.handlersKeys.add(filter);
-        }
-    }
-
-    /**
-     * Remove the handler associated with the given filter
-     * 
-     * @param filter
-     *            The filter to remove
-     * @return true if successful, false otherwise
-     */
-    public boolean removeHandler(IRCMessageFilter filter) {
-        synchronized (this.handlers) {
-            if (this.handlers.containsKey(filter)) {
-                this.handlers.remove(filter);
-                this.handlersKeys.remove(filter);
-                return true;
-            }
-
-            return false;
-        }
-    }
-
-    /**
-     * Return the IRCChannelHandler for this client connection
-     * 
-     * @return the IRCChannelHandler for this client connection
-     */
-    public IRCChannelHandler getConnection() {
-        return this.channelHandler;
-    }
-
-    @Override
-    public void onClose(IRCChannelHandler connection) {
-        /*
-         * With the underly connection closed we can safely shutdown the thread
-         * pool
-         */
-        log.debug("on close");
-        synchronized (this.handlers) {
-            this.threadPool.shutdown();
-        }
-    }
-
-    @Override
-    public void onShutdown() {
-        log.debug("on shutdown");
-        this.bootstrap.releaseExternalResources();
-    }
-
-    @Override
-    public void receiveMessage(IRCChannelHandler connection, IRCMessage message) {
-        switch (message.getType()) {
-
-        /*
-         * At least one of these should be received once a good connection is
-         * established
-         */
-        case RPL_ENDOFMOTD:
-        case RPL_LUSERME:
-        case RPL_LUSERCHANNELS:
-        case RPL_LUSERCLIENT:
-        case RPL_LUSEROP:
-        case RPL_LUSERUNKNOWN:
-            synchronized (this.connected) {
-                this.connectedSuccessfully = true;
-                this.connected.countDown();
-            }
-            break;
-
-        /* Error while connecting */
-        case ERR_NICKNAMEINUSE:
-            synchronized (this.connected) {
-                this.connectedSuccessfully = false;
-                this.connected.countDown();
-            }
-            break;
-
-        case PING:
-            IRCMessage pong = new IRCMessage(IRCMessageType.PONG, this.servername);
-            connection.sendMessage(pong);
-            break;
-
-        default:
-            break;
-        }
-
-        synchronized (this.handlers) {
-            if (this.threadPool.isShutdown()) {
-                return;
-            }
-
-            for (IRCMessageFilter filter : this.handlersKeys) {
-                if (filter.check(message)) {
-                    IRCMessageHandler handler = this.handlers.get(filter);
-                    MessageHandlerRunnable runnableHandler = new MessageHandlerRunnable(handler, message);
-                    this.threadPool.submit(runnableHandler);
-                }
-            }
-        }
-    }
-}

brewtab-irc/src/main/java/com/brewtab/irc/IRCConnectionManager.java

-package com.brewtab.irc;
-
-import com.brewtab.irc.messages.IRCMessage;
-import com.brewtab.irc.protocol.IRCChannelHandler;
-
-/**
- * A class implementing this interface can be used to manage a IRC connection
- * through an IRCChannelHandler object. Typically, a class implementing this
- * interface will be passed to a IRCChannelPipelineFactory as either an instance
- * or through an IRCConnectionManagerFactroy to actually manage a connection.
- * 
- * @author Christopher Thunes <cthunes@brewtab.com>
- */
-public interface IRCConnectionManager {
-    /**
-     * Called by the underlying IRChannelHandler once the connection has been
-     * established
-     * 
-     * @param connection
-     *            The associated IRCChannelHandler
-     */
-    public void onConnect(IRCChannelHandler connection);
-
-    /**
-     * Called by the underlying IRCChannelHandler once a close request has been
-     * made on the channel
-     * 
-     * @param connection
-     */
-    public void onClose(IRCChannelHandler connection);
-
-    /**
-     * Called by the underlying IRCChannelHandler once the IRCChannelHandler is
-     * shut down
-     */
-    public void onShutdown();
-
-    /**
-     * Called by the underlying IRCChannelHandler whenever it receive a message
-     * 
-     * @param connection
-     *            The calling IRCChannelHandler
-     * @param message
-     *            The received message
-     */
-    public void receiveMessage(IRCChannelHandler connection, IRCMessage message);
-}

brewtab-irc/src/main/java/com/brewtab/irc/IRCConnectionManagerFactory.java

-package com.brewtab.irc;
-
-/**
- * A factory for IRCConnectionManager objects. An IRCConnectionManagerFactory
- * can be used to provide separate IRCConnectionManager objects per pipeline.
- * Typical usage case would be to provide separate managers to each client
- * connection for a server.
- * 
- * @author Christopher Thunes <cthunes@brewtab.com>
- */
-public interface IRCConnectionManagerFactory {
-    /**
-     * Return a new IRCConnectionManager
-     * 
-     * @return the new connection manager
-     */
-    public IRCConnectionManager getConnectionManager();
-}

brewtab-irc/src/main/java/com/brewtab/irc/IRCMessageHandler.java

-package com.brewtab.irc;
-
-import com.brewtab.irc.messages.IRCMessage;
-
-/**
- * Implementing class are capable of being registered with other objects to
- * receive upstream messages
- * 
- * @author Christopher Thunes <cthunes@brewtab.com>
- */
-public interface IRCMessageHandler {
-    /**
-     * Handle a message
-     * 
-     * @param message
-     *            The message to process
-     */
-    public void handleMessage(IRCMessage message);
-}

brewtab-irc/src/main/java/com/brewtab/irc/IRCPrivateChat.java

-package com.brewtab.irc;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.brewtab.irc.messages.IRCMessage;
-import com.brewtab.irc.messages.IRCMessageType;
-import com.brewtab.irc.messages.filter.IRCMessageFilter;
-import com.brewtab.irc.messages.filter.IRCMessageSimpleFilter;
-import com.brewtab.irc.messages.filter.IRCUserPrefixFilter;
-
-/**
- * Have a private conversation
- * 
- * @author Christopher Thunes <cthunes@brewtab.com>
- */
-public class IRCPrivateChat implements IRCMessageHandler {
-    private final static Logger log = LoggerFactory.getLogger(IRCPrivateChat.class);
-
-    /* Nick of the user receiving the messages */
-    private String receiver;
-
-    /* Associated IRCClient object */
-    private IRCClient client;
-
-    public IRCPrivateChat(IRCClient client, String nick) {
-        this.client = client;
-        this.receiver = nick;
-
-        IRCMessageFilter filter = new IRCMessageSimpleFilter(IRCMessageType.PRIVMSG, new IRCUserPrefixFilter(nick));
-        this.client.addHandler(filter, this);
-    }
-
-    public String getReceiver() {
-        return this.receiver;
-    }
-
-    /**
-     * Get the associated IRCClient
-     * 
-     * @return the associated IRCClient
-     */
-    public IRCClient getClient() {
-        return this.client;
-    }
-
-    /**
-     * Write a message to the channel
-     * 
-     * @param text
-     *            The message to send
-     */
-    public void write(String text) {
-        IRCMessage privMessage = new IRCMessage(IRCMessageType.PRIVMSG, this.receiver, text);
-        this.client.getConnection().sendMessage(privMessage);
-    }
-
-    /**
-     * Write multiple messages efficiently
-     * 
-     * @param strings
-     *            The messages to send
-     */
-    public void writeMultiple(String... strings) {
-        IRCMessage[] privMessages = new IRCMessage[strings.length];
-        for (int i = 0; i < strings.length; i++) {
-            privMessages[i] = new IRCMessage(IRCMessageType.PRIVMSG, this.receiver, strings[i]);
-        }
-        this.client.getConnection().sendMessages(privMessages);
-    }
-
-    @Override
-    public void handleMessage(IRCMessage message) {
-        IRCUser user = IRCUser.fromString(message.getPrefix());
-        String messageText = message.getArgs()[message.getArgs().length - 1];
-
-        log.debug("PM from {}: {}", user.getNick(), messageText);
-    }
-}

brewtab-irc/src/main/java/com/brewtab/irc/IRCUser.java

-package com.brewtab.irc;
-
-/**
- * Simple object encapsulating a user's nick, username, and hostname
- * 
- * @author Christopher Thunes <cthunes@brewtab.com>
- */
-public class IRCUser {
-    /** The nick */
-    private String nick;
-
-    /** The user */
-    private String user;
-
-    /** The host */
-    private String host;
-
-    /**
-     * Construct a new IRCUser with the given parameters
-     * 
-     * @param nick
-     *            The user's nick
-     * @param user
-     *            The user's username
-     * @param host
-     *            The user's hostname
-     */
-    public IRCUser(String nick, String user, String host) {
-        this.nick = nick;
-        this.user = user;
-        this.host = host;
-    }
-
-    /**
-     * Get the nick
-     * 
-     * @return the nick
-     */
-    public String getNick() {
-        return this.nick;
-    }
-
-    /**
-     * Return the user name
-     * 
-     * @return the user name
-     */
-    public String getUser() {
-        return this.user;
-    }
-
-    /**
-     * Return the host name
-     * 
-     * @return the host name
-     */
-    public String getHost() {
-        return this.host;
-    }
-
-    /**
-     * Construct a new IRCUser from the give String. The String should be in the
-     * format {@literal <nick>!<user>@<host>}. This format is the same as is
-     * used in IRC message prefixes
-     * 
-     * @param prefix
-     *            The prefix to extract the information from
-     * @return a new IRCUser object or null if the prefix could not be parsed
-     */
-    public static IRCUser fromString(String prefix) {
-        if (prefix == null) {
-            return null;
-        }
-
-        int endNick = prefix.indexOf('!');
-        int endUser = prefix.indexOf('@');
-
-        if (endNick == -1 || endUser == -1 || endUser < endNick) {
-            return null;
-        }
-
-        String nick = prefix.substring(0, endNick);
-        String user = prefix.substring(endNick + 1, endUser);
-        String host = prefix.substring(endUser + 1);
-
-        return new IRCUser(nick, user, host);
-    }
-}

brewtab-irc/src/main/java/com/brewtab/irc/NotConnectedException.java

+package com.brewtab.irc;
+
+/**
+ * Thrown to indicate that an operation was request that could be not be
+ * completed because a connection has not been established
+ * 
+ * @author Christopher Thunes <cthunes@brewtab.com>
+ */
+public class NotConnectedException extends IllegalStateException {
+    private static final long serialVersionUID = 4861994587138843189L;
+
+    /**
+     * Construct a new IRCNotConnectedException with the default message
+     */
+    public NotConnectedException() {
+        super("Not connected");
+    }
+
+    /**
+     * Construct a new IRCNotConnectedException with the give message
+     * 
+     * @param reason
+     *            The message to store along with the exception
+     */
+    public NotConnectedException(String reason) {
+        super(reason);
+    }
+}

brewtab-irc/src/main/java/com/brewtab/irc/User.java

+package com.brewtab.irc;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Simple object encapsulating a user's nick, username, and hostname
+ * 
+ * @author Christopher Thunes <cthunes@brewtab.com>
+ */
+public class User {
+    private static final Pattern userPrefixPattern;
+
+    static {
+        final String specialNickCharset = Pattern.quote("[]\\`_^{}|");
+        final String nickFirstCharacter = "[a-zA-Z" + specialNickCharset + "]";
+        final String nickRest = "[a-zA-Z0-9\\-" + specialNickCharset + "]*";
+
+        final String nick = nickFirstCharacter + nickRest;
+        final String user = "[^@]+";
+        final String host = ".+";
+
+        userPrefixPattern = Pattern.compile("^(" + nick + ")((!(" + user + "))?@(" + host + "))?$");
+    }
+
+    /** The nick */
+    private String nick;
+
+    /** The user */
+    private String user;
+
+    /** The host */
+    private String host;
+
+    /**
+     * Construct a new IRCUser with the given parameters. Only a nick is
+     * required, but if a user is provided a host must also be provided.
+     * 
+     * @param nick The user's nick
+     * @param user The user's username, or null for none
+     * @param host The user's hostname, or null for none
+     */
+    public User(String nick, String user, String host) {
+        if (nick == null) {
+            throw new IllegalArgumentException("nick can not be null");
+        }
+
+        if (user != null && host == null) {
+            throw new IllegalArgumentException("can not provide username without hostname");
+        }
+
+        this.nick = nick;
+        this.user = user;
+        this.host = host;
+    }
+
+    public User(String nick) {
+        this(nick, null, null);
+    }
+
+    public User(String nick, String host) {
+        this(nick, null, host);
+    }
+
+    /**
+     * Get the nick
+     * 
+     * @return the nick
+     */
+    public String getNick() {
+        return this.nick;
+    }
+
+    /**
+     * Return the user name
+     * 
+     * @return the user name
+     */
+    public String getUser() {
+        return this.user;
+    }
+
+    /**
+     * Return the host name
+     * 
+     * @return the host name
+     */
+    public String getHost() {
+        return this.host;
+    }
+
+    /**
+     * Construct a new IRCUser from the give String. The String should be in the
+     * format {@literal <nick>!<user>@<host>}. This format is the same as is
+     * used in IRC message prefixes
+     * 
+     * @param prefix The prefix to extract the information from
+     * @return a new IRCUser object or null if the prefix could not be parsed
+     */
+    public static User fromPrefix(String prefix) {
+        Matcher matcher = userPrefixPattern.matcher(prefix);
+
+        if (matcher.find()) {
+            String nick = matcher.group(1);
+            String user = matcher.group(4);
+            String host = matcher.group(5);
+
+            return new User(nick, user, host);
+        } else {
+            return null;
+        }
+    }
+
+    public String toPrefix() {
+        StringBuilder sb = new StringBuilder();
+
+        sb.append(nick);
+
+        if (host != null) {
+            if (user != null) {
+                sb.append('!');
+                sb.append(user);
+            }
+
+            sb.append('@');
+            sb.append(host);
+        }
+
+        return sb.toString();
+    }
+}

brewtab-irc/src/main/java/com/brewtab/irc/client/Channel.java

+package com.brewtab.irc.client;
+
+import java.util.List;
+
+public interface Channel {
+
+    /**
+     * Get the channel name
+     * 
+     * @return the channel name
+     */
+    public String getName();
+
+    /**
+     * Get the associated IRCClient
+     * 
+     * @return the associated IRCClient
+     */
+    public Client getClient();
+
+    /**
+     * Part the channel
+     * 
+     * @param reason The reason sent with the PART message
+     */
+    public void part(String reason);
+
+    /**
+     * Write a message to the channel
+     * 
+     * @param text The message to send
+     */
+    public void write(String text);
+
+    /**
+     * Write multiple messages efficiently
+     * 
+     * @param strings The messages to send
+     */
+    public void writeMultiple(String... strings);
+
+    /**
+     * Retrieve the list of users in the channel. This call returns a cached
+     * copy. To request a refresh of the listing from the server call
+     * IRChannel#refreshNames.
+     * 
+     * @return the list of nicks of users in the channel
+     */
+    public List<String> getNames();
+
+    /**
+     * Add an IRCChannelListener to this channel.
+     * 
+     * @param listener the listener to add
+     */
+    public void addListener(ChannelListener listener);
+
+}

brewtab-irc/src/main/java/com/brewtab/irc/client/ChannelListener.java

+package com.brewtab.irc.client;
+
+import com.brewtab.irc.User;
+
+/**
+ * Implementing class can listen for IRC channel related events
+ * 
+ * @author Christopher Thunes <cthunes@brewtab.com>
+ */
+public interface ChannelListener {
+    /**
+     * Called when a user joins the channel
+     * 
+     * @param channel The channel that was joined
+     * @param user The user that joined
+     */
+    public void onJoin(Channel channel, User user);
+
+    /**
+     * Called when a user parts the channel
+     * 
+     * @param channel The channel that was parted from
+     * @param user The user that parted
+     */
+    public void onPart(Channel channel, User user);
+
+    /**
+     * Called when a user quits the channel
+     * 
+     * @param channel The channel that was quit from
+     * @param user The user that parted
+     */
+    public void onQuit(Channel channel, User user);
+
+    /**
+     * Called whenever a message is sent to the channel
+     * 
+     * @param channel The channel the message was received on
+     * @param from The user that sent the message
+     * @param message The message
+     */
+    public void onMessage(Channel channel, User from, String message);
+}

brewtab-irc/src/main/java/com/brewtab/irc/client/Client.java

+package com.brewtab.irc.client;
+
+import com.brewtab.irc.Connection;
+import com.brewtab.irc.User;
+
+public interface Client {
+    public void setUsername(String username);
+
+    public String getUsername();
+
+    public void setHostname(String hostname);
+
+    public String getHostname();
+
+    public void setRealName(String realName);
+
+    public String getRealName();
+
+    public void setNick(String nick);
+
+    public String getNick();
+
+    public void setPassword(String password);
+
+    public String getPassword();
+
+    /**
+     * Connect to the server.
+     * 
+     * @param nick
+     */
+    public void connect();
+
+    /**
+     * Quit and disconnect from the server
+     * 
+     * @param message
+     */
+    public void quit(String message);
+
+    /**
+     * Quit and disconnect from the server using a default message.
+     */
+    public void quit();
+
+    /**
+     * Join a channel
+     * 
+     * @param channelName
+     * @return
+     */
+    public Channel join(String channelName);
+
+    /**
+     * Send a message to a user
+     * 
+     * @param nick
+     * @param message
+     */
+    public void sendMessage(User user, String message);
+
+    /**
+     * Get the connection used by the client object
+     * 
+     * @return
+     */
+    public Connection getConnection();
+}

brewtab-irc/src/main/java/com/brewtab/irc/client/ClientFactory.java

+package com.brewtab.irc.client;
+
+import java.net.InetSocketAddress;
+
+import com.brewtab.irc.impl.ClientFactoryImpl;
+
+public class ClientFactory {
+    public static Client newClient(InetSocketAddress address) {
+        return ClientFactoryImpl.newClient(address);
+    }
+}

brewtab-irc/src/main/java/com/brewtab/irc/client/NickNameInUseException.java

+package com.brewtab.irc.client;
+
+public class NickNameInUseException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+}

brewtab-irc/src/main/java/com/brewtab/irc/impl/ChannelImpl.java

+package com.brewtab.irc.impl;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+import com.brewtab.irc.Connection;
+import com.brewtab.irc.User;
+import com.brewtab.irc.client.Channel;
+import com.brewtab.irc.client.ChannelListener;
+import com.brewtab.irc.client.Client;
+import com.brewtab.irc.messages.Message;
+import com.brewtab.irc.messages.MessageListener;
+import com.brewtab.irc.messages.MessageType;
+import com.brewtab.irc.messages.filter.MessageFilters;
+
+/**
+ * An IRC channel. Represents a connection to an IRC channel.
+ * 
+ * @author Christopher Thunes <cthunes@brewtab.com>
+ */
+class ChannelImpl implements MessageListener, Channel {
+    /* Channel name */
+    private String channelName;
+
+    /* IRC Client */
+    private Client client;
+
+    /* Associated IRCConnection object */
+    private Connection connection;
+
+    /* Set once the channel has been joined */
+    private CountDownLatch joined;
+
+    /* IRCChanneListners for this channel */
+    private List<ChannelListener> listeners;
+
+    /* List of nicks in the channel */
+    private List<String> names;
+
+    /**
+     * Instantiate a new IRCChannel object associated with the given IRCClient
+     * 
+     * @param client The client to operate with
+     * @param channelName The channel to join
+     */
+    public ChannelImpl(Client client, String channelName) {
+        this.client = client;
+        this.connection = client.getConnection();
+        this.channelName = channelName;
+        this.joined = new CountDownLatch(1);
+        this.listeners = new LinkedList<ChannelListener>();
+        this.names = new ArrayList<String>();
+
+        /*
+         * Build a compound filter collecting messages sent regarding this
+         * channel and include all QUIT messages. Some QUIT messages will
+         * concern this channel if it's a user in this channel QUITing
+         */
+
+        connection.addMessageListener(
+            MessageFilters.any(
+                // Messages targeted to the channel
+                MessageFilters.message(MessageType.PRIVMSG, channelName),
+                MessageFilters.message(MessageType.JOIN, channelName),
+                MessageFilters.message(MessageType.PART, channelName),
+                MessageFilters.message(MessageType.RPL_TOPIC, channelName),
+                MessageFilters.message(MessageType.RPL_NOTOPIC, channelName),
+                MessageFilters.message(MessageType.RPL_NAMREPLY, null, "=", channelName),
+                MessageFilters.message(MessageType.RPL_ENDOFNAMES, null, channelName),
+
+                // Messages which may be relevant to our channel
+                MessageFilters.message(MessageType.QUIT),
+                MessageFilters.message(MessageType.NICK)),
+            this);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.brewtab.irc.impl.IRCChannel#getName()
+     */
+    @Override
+    public String getName() {
+        return this.channelName;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.brewtab.irc.impl.IRCChannel#getClient()
+     */
+    @Override
+    public Client getClient() {
+        return this.client;
+    }
+
+    /**
+     * Perform the join to the channel. Returns once the join operation is
+     * complete
+     * 
+     * @return true if joined successfully, false otherwise
+     */
+    public boolean join() {
+        Message joinMessage = new Message(MessageType.JOIN, this.channelName);
+        List<Message> response;
+
+        try {
+            response = connection.request(
+                MessageFilters.message(MessageType.RPL_NAMREPLY, null, "=", channelName),
+                MessageFilters.message(MessageType.RPL_ENDOFNAMES, null, channelName),
+                joinMessage);
+        } catch (InterruptedException e) {
+            return false;
+        }
+
+        List<String> names = new ArrayList<String>();
+
+        for (Message message : response) {
+            if (message.getType() == MessageType.RPL_NAMREPLY) {
+                String[] args = message.getArgs();
+
+                for (String name : args[args.length - 1].split(" ")) {
+                    names.add(name.replaceFirst("^[@+]", ""));
+                }
+            }
+        }
+
+        this.names = names;
+
+        return true;
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.brewtab.irc.impl.IRCChannel#part(java.lang.String)
+     */
+    @Override
+    public void part(String reason) {
+        Message partMessage = new Message(MessageType.PART, this.channelName, reason);
+        this.client.getConnection().send(partMessage);
+        this.joined = new CountDownLatch(1);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.brewtab.irc.impl.IRCChannel#write(java.lang.String)
+     */
+    @Override
+    public void write(String text) {
+        Message privMessage = new Message(MessageType.PRIVMSG, this.channelName, text);
+        this.client.getConnection().send(privMessage);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.brewtab.irc.impl.IRCChannel#writeMultiple(java.lang.String)
+     */
+    @Override
+    public void writeMultiple(String... strings) {
+        Message[] privMessages = new Message[strings.length];
+        for (int i = 0; i < strings.length; i++) {
+            privMessages[i] = new Message(MessageType.PRIVMSG, this.channelName, strings[i]);
+        }
+        this.client.getConnection().send(privMessages);
+    }
+
+    /*
+     * (non-Javadoc)
+     * @see com.brewtab.irc.impl.IRCChannel#getNames()
+     */
+    @Override
+    public List<String> getNames() {
+        return names;
+    }
+
+    /**
+     * Makes a NAMES request to the server for this channel. Store the result
+     * replacing any existing names list. The list can be retrieved with
+     * IRCChannel#getNames
+     */
+    public void refreshNames() {
+        Message namesMessage = new Message(MessageType.NAMES, this.channelName);
+        List<Message> response;
+
+        try {
+            response = connection.request(
+                MessageFilters.message(MessageType.RPL_NAMREPLY, null, "=", channelName),
+                MessageFilters.message(MessageType.RPL_ENDOFNAMES, null, channelName),
+                namesMessage);
+        } catch (InterruptedException e) {
+            return;
+        }
+
+        List<String> names = new ArrayList<String>();
+
+        for (Message message : response) {
+            if (message.getType() == MessageType.RPL_NAMREPLY) {
+                String[] args = message.getArgs();
+
+                for (String name : args[args.length - 1].split(" ")) {
+                    names.add(name.replaceFirst("^[@+]", ""));
+                }
+            }