<commit_before>/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package net.java.sip.communicator.impl.protocol.irc;

import java.util.*;

import net.java.sip.communicator.impl.protocol.irc.ModeParser.ModeEntry;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;

import com.ircclouds.irc.api.*;
import com.ircclouds.irc.api.domain.*;
import com.ircclouds.irc.api.domain.messages.*;
import com.ircclouds.irc.api.domain.messages.interfaces.*;
import com.ircclouds.irc.api.listeners.*;
import com.ircclouds.irc.api.state.*;

/**
 * An implementation of IRC using the irc-api library.
 * 
 * @author <NAME>
 * 
 * TODO Make irc-api OSGi aware.
 */
public class IrcStack
{
    /**
     * Logger
     */
    private static final Logger LOGGER = Logger.getLogger(IrcStack.class);

    /**
     * Parent provider for IRC
     */
    private final ProtocolProviderServiceIrcImpl provider;

    // TODO should make this thing a map or set?
    /**
     * Container for joined channels.
     */
    private final List<ChatRoomIrcImpl> joined = Collections
        .synchronizedList(new ArrayList<ChatRoomIrcImpl>());

    /**
     * Server parameters that are set and provided during the connection
     * process.
     */
    private final ServerParameters params;

    /**
     * Instance of the IRC library.
     */
    private IRCApi irc;

    /**
     * Connection state of a successful IRC connection.
     */
    private IIRCState connectionState;
    
    /**
     * The cached channel list.
     * 
     * Contained inside a simple container object in order to lock the container
     * while accessing the contents.
     */
    private final Container<List<String>> channellist =
        new Container<List<String>>(null);

    /**
     * Constructor
     * 
     * @param parentProvider Parent provider
     * @param nick User's nick name
     * @param login User's login name
     * @param version Version
     * @param finger Finger
     */
    public IrcStack(final ProtocolProviderServiceIrcImpl parentProvider,
        final String nick, final String login, final String version,
        final String finger)
    {
        if (parentProvider == null)
        {
            throw new NullPointerException("parentProvider cannot be null");
        }
        this.provider = parentProvider;
        this.params = new IrcStack.ServerParameters(nick, login, finger, null);
    }

    /**
     * Check whether or not a connection is established.
     * 
     * @return true if connected, false otherwise.
     */
    public boolean isConnected()
    {
        return (this.irc != null && this.connectionState != null && this.connectionState
            .isConnected());
    }

    /**
     * Check whether the connection is a secure connection (TLS).
     * 
     * @return true if connection is secure, false otherwise.
     */
    public boolean isSecureConnection()
    {
        return isConnected() && this.connectionState.getServer().isSSL();
    }

    /**
     * Connect to specified host, port, optionally using a password.
     * 
     * @param host IRC server's host name
     * @param port IRC port
     * @param password
     * @param autoNickChange
     * @throws Exception
     */
    public void connect(String host, int port, String password,
        boolean secureConnection, boolean autoNickChange) throws Exception
    {
        if (this.irc != null && this.connectionState != null
            && this.connectionState.isConnected())
            return;

        // Make sure we start with an empty joined-channel list.
        this.joined.clear();

        // A container for storing the exception if connecting fails.
        final Exception[] exceptionContainer = new Exception[1];

        this.irc = new IRCApiImpl(true);
        // FIXME Currently, the secure connection is created by
        // explicitly creating an SSLContext for 'SSL'. According
        // to Ingo (in a mailing list conversation) it is better to
        // use the CertificateService for this. This should be
        // implemented in the irc-api library, though.
        this.params.setServer(new IRCServer(host, port, password,
            secureConnection));
        synchronized (this.irc)
        {
            // register a server listener in order to catch server and
            // cross-/multi-channel messages
            this.irc.addListener(new ServerListener());
            // start connecting to the specified server ...
            // TODO Catch IOException/SocketException in case of early failure
            // in call to connect()
            this.irc.connect(this.params, new Callback<IIRCState>()
            {

                @Override
                public void onSuccess(IIRCState state)
                {
                    synchronized (IrcStack.this.irc)
                    {
                        System.out.println("IRC connected successfully!");
                        IrcStack.this.connectionState = state;
                        IrcStack.this.irc.notifyAll();
                    }
                }

                @Override
                public void onFailure(Exception e)
                {
                    synchronized (IrcStack.this.irc)
                    {
                        System.out.println("IRC connection FAILED! ("
                            + e.getMessage() + ")");
                        exceptionContainer[0] = e;
                        IrcStack.this.connectionState = null;
                        IrcStack.this.irc.notifyAll();
                    }
                }
            });

            // wait while the irc connection is being established ...
            try
            {
                System.out
                    .println("Waiting for the connection to be established ...");
                this.irc.wait();
                if (this.connectionState != null
                    && this.connectionState.isConnected())
                {
                    this.provider
                        .setCurrentRegistrationState(RegistrationState.REGISTERED);
                }
                else
                {
                    this.provider
                        .setCurrentRegistrationState(RegistrationState.UNREGISTERED);

                    if (exceptionContainer[0] != null)
                    {
                        // If an exception happens to be available that explains
                        // the connection problem, throw it.
                        throw exceptionContainer[0];
                    }
                }
            }
            catch (InterruptedException e)
            {
                this.provider
                    .setCurrentRegistrationState(RegistrationState.UNREGISTERED);
                throw e;
            }
        }
    }

    /**
     * Disconnect from the IRC server
     */
    public void disconnect()
    {
        if (this.connectionState == null && this.irc == null)
            return;
        
        synchronized(this.joined)
        {
            for (ChatRoomIrcImpl channel : this.joined)
            {
                leave(channel);
            }
        }
        this.irc.disconnect();
        this.irc = null;
        this.connectionState = null;
        this.provider
            .setCurrentRegistrationState(RegistrationState.UNREGISTERED);
    }

    /**
     * Dispose
     */
    public void dispose()
    {
        disconnect();
    }

    /**
     * Get the nick name of the user.
     * 
     * @return Returns either the acting nick if a connection is established or
     *         the configured nick.
     */
    public String getNick()
    {
        return (this.connectionState == null) ? this.params.getNickname()
            : this.connectionState.getNickname();
    }

    /**
     * Set the user's new nick name.
     * 
     * @param nick the new nick name
     */
    public void setUserNickname(String nick)
    {
        if (this.connectionState == null)
        {
            this.params.setNickname(nick);
        }
        else
        {
            this.irc.changeNick(nick);
        }
    }

    /**
     * Set the subject of the specified chat room.
     * 
     * @param chatroom The chat room for which to set the subject.
     * @param subject The subject.
     */
    public void setSubject(ChatRoomIrcImpl chatroom, String subject)
    {
        if (isConnected() == false)
            throw new IllegalStateException(
                "Please connect to an IRC server first.");
        if (chatroom == null)
            throw new IllegalArgumentException("Cannot have a null chatroom");
        this.irc
            .changeTopic(chatroom.getName(), subject == null ? "" : subject);
    }

    /**
     * Check whether the user has joined a particular chat room.
     * 
     * @param chatroom Chat room to check for.
     * @return Returns true in case the user is already joined, or false if the
     *         user has not joined.
     */
    public boolean isJoined(ChatRoomIrcImpl chatroom)
    {
        return this.joined.contains(chatroom);
    }

    /**
     * Get a list of channels available on the IRC server.
     * 
     * @return List of available channels.
     */
    public List<String> getServerChatRoomList()
    {
        LOGGER.trace("Start retrieve server chat room list.");
        // FIXME Currently, not using an API library method for listing
        // channels, since it isn't available.
        synchronized (this.channellist)
        {
            if (this.channellist.instance == null)
            {
                List<String> list = new LinkedList<String>();
                synchronized (list)
                {
                    try
                    {
                        this.irc.addListener(new ChannelListListener(this.irc,
                            list));
                        this.irc.rawMessage("LIST");
                        LOGGER.trace("Start waiting for list ...");
                        list.wait();
                        LOGGER.trace("Done waiting for list.");
                    }
                    catch (InterruptedException e)
                    {
                        LOGGER.trace("INTERRUPTED while waiting for list.");
                        e.printStackTrace();
                    }
                }
                this.channellist.instance = list;
            }
            LOGGER.trace("Finished retrieve server chat room list.");
            return Collections.unmodifiableList(this.channellist.instance);
        }
    }

    /**
     * Join a particular chat room.
     * 
     * @param chatroom Chat room to join.
     */
    public void join(ChatRoomIrcImpl chatroom)
    {
        join(chatroom, "");
    }

    /**
     * Join a particular chat room.
     * 
     * Issue a join channel IRC operation and wait for the join operation to
     * complete (either successfully or failing).
     * 
     * @param chatroom The chatroom to join.
     * @param password Optionally, a password that may be required for some
     *            channels.
     */
    public void join(final ChatRoomIrcImpl chatroom, final String password)
    {
        if (isConnected() == false)
            throw new IllegalStateException(
                "Please connect to an IRC server first");
        if (chatroom == null)
            throw new IllegalArgumentException("chatroom cannot be null");
        if (password == null)
            throw new IllegalArgumentException("password cannot be null");
        if (this.joined.contains(chatroom) || chatroom.isPrivate())
        {
            // If we already joined this particular chatroom or if it is a
            // private chat room (i.e. message from one user to another), no
            // further action is required.
            return;
        }

        LOGGER.trace("Start joining channel " + chatroom.getIdentifier());
        final Object joinSignal = new Object();
        synchronized (joinSignal)
        {
            try
            {
                LOGGER
                    .trace("Issue join channel command to IRC library and wait for join operation to complete (un)successfully.");
                // TODO Refactor this ridiculous nesting of functions and
                // classes.
                this.irc.joinChannel(chatroom.getIdentifier(), password,
                    new Callback<IRCChannel>()
                    {

                        @Override
                        public void onSuccess(IRCChannel channel)
                        {
                            synchronized (joinSignal)
                            {
                                try
                                {
                                    // TODO Handle forward to another channel
                                    // (470) channel name change.
                                    // (Testable on irc.freenode.net#linux,
                                    // forwards to ##linux)
                                    // Currently drops into an infinite wait
                                    // because the callback is never
                                    // executed. Not sure how to catch/detect
                                    // this forward operation and act
                                    // appropriately. Seems reasonable to just
                                    // expect the same callback, but
                                    // with different channel information.
                                    IrcStack.this.joined.add(chatroom);
                                    IrcStack.this.irc
                                        .addListener(new ChatRoomListener(
                                            chatroom));
                                    IRCTopic topic = channel.getTopic();
                                    chatroom.updateSubject(topic.getValue());

                                    for (IRCUser user : channel.getUsers())
                                    {
                                        ChatRoomMemberRole role =
                                            ChatRoomMemberRole.SILENT_MEMBER;
                                        Set<IRCUserStatus> statuses =
                                            channel.getStatusesForUser(user);
                                        for (IRCUserStatus status : statuses)
                                        {
                                            role =
                                                convertMemberMode(status
                                                    .getChanModeType()
                                                    .charValue());
                                        }

                                        if (IrcStack.this.getNick().equals(
                                            user.getNick()))
                                        {
                                            chatroom.prepUserRole(role);
                                        }
                                        else
                                        {
                                            ChatRoomMemberIrcImpl member =
                                                new ChatRoomMemberIrcImpl(
                                                    IrcStack.this.provider,
                                                    chatroom, user.getNick(),
                                                    role);
                                            chatroom.addChatRoomMember(
                                                member.getContactAddress(),
                                                member);
                                        }
                                    }
                                }
                                finally
                                {
                                    // In any case, issue the local user
                                    // presence, since the irc library notified
                                    // us of a successful join. We should wait
                                    // as long as possible though. First we need
                                    // to fill the list of chat room members and
                                    // other chat room properties.
                                    IrcStack.this.provider
                                        .getMUC()
                                        .fireLocalUserPresenceEvent(
                                            chatroom,
                                            LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED,
                                            null);
                                    LOGGER
                                        .trace("Finished successful join callback. Waking up original thread.");
                                    // Notify waiting threads of finished
                                    // execution.
                                    joinSignal.notifyAll();
                                }
                            }
                        }

                        @Override
                        public void onFailure(Exception e)
                        {
                            // TODO how should we communicate a failed attempt
                            // at joining the channel?
                            synchronized (joinSignal)
                            {
                                try
                                {
                                    MessageIrcImpl message =
                                        new MessageIrcImpl(
                                            "Failed to join channel "
                                                + chatroom.getIdentifier()
                                                + " (" + e.getMessage() + ")",
                                            "text/plain", "UTF-8",
                                            "Failed to join");
                                    chatroom
                                        .fireMessageReceivedEvent(
                                            message,
                                            null,
                                            new Date(),
                                            MessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED);
                                }
                                finally
                                {
                                    LOGGER
                                        .trace("Finished unsuccessful join callback. Waking up original thread.");
                                    // Notify waiting threads of finished
                                    // execution
                                    joinSignal.notifyAll();
                                }
                            }
                        }
                    });
                // Wait until async channel join operation has finished.
                joinSignal.wait();
                LOGGER
                    .trace("Finished waiting for join operation to complete.");
                // TODO How to handle 471 (+l): Channel is full (reached set
                // user limit)?
                // TODO How to handle 480 (+j): Channel throttle exceeded?
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

    /**
     * Part from a joined chat room.
     * 
     * @param chatroom The chat room to part from.
     */
    public void leave(ChatRoomIrcImpl chatroom)
    {
        if (chatroom.isPrivate() == false)
        {
            // You only actually join non-private chat rooms, so only these ones
            // need to be left.
            leave(chatroom.getIdentifier());
        }
    }

    /**
     * Part from a joined chat room.
     * 
     * @param chatRoomName The chat room to part from.
     */
    private void leave(String chatRoomName)
    {
        this.irc.leaveChannel(chatRoomName);
    }

    public void banParticipant(ChatRoomIrcImpl chatroom, ChatRoomMember member,
        String reason)
    {
        // TODO Implement this.
    }

    /**
     * Kick channel member.
     * 
     * @param chatroom channel to kick from
     * @param member member to kick
     * @param reason kick message to deliver
     */
    public void kickParticipant(ChatRoomIrcImpl chatroom,
        ChatRoomMember member, String reason)
    {
        this.irc.kick(chatroom.getIdentifier(), member.getContactAddress(),
            reason);
    }

    /**
     * Issue invite command to IRC server.
     * 
     * @param memberId member to invite
     * @param chatroom channel to invite to
     */
    public void invite(String memberId, ChatRoomIrcImpl chatroom)
    {
        this.irc.rawMessage("INVITE " + memberId + " "
            + chatroom.getIdentifier());
    }

    /**
     * Send a command to the IRC server.
     * @param chatroom the chat room
     * @param command the command message
     */
    public void command(ChatRoomIrcImpl chatroom, String command)
    {
        String target;
        if (command.toLowerCase().startsWith("/msg "))
        {
            command = command.substring(5);
            int endOfNick = command.indexOf(' ');
            if (endOfNick == -1)
            {
                throw new IllegalArgumentException("Invalid private message format. Message was not sent.");
            }
            target = command.substring(0,  endOfNick);
            command = command.substring(endOfNick+1);
        }
        else
        {
            target = chatroom.getIdentifier();
        }
        this.irc.message(target, command);
    }

    /**
     * Send an IRC message.
     * 
     * @param chatroom The chat room to send the message to.
     * @param message The message to send.
     */
    public void message(ChatRoomIrcImpl chatroom, String message)
    {
        String target = chatroom.getIdentifier();
        this.irc.message(target, message);
    }

    /**
     * Convert a member mode character to a ChatRoomMemberRole instance.
     * 
     * @param modeSymbol The member mode character.
     * @return Return the instance of ChatRoomMemberRole corresponding to the
     *         member mode character.
     */
    private static ChatRoomMemberRole convertMemberMode(char modeSymbol)
    {
        return Mode.bySymbol(modeSymbol).getRole();
    }

    /**
     * A listener for server-level messages (any messages that are related to the
     * server, the connection, that are not related to any chatroom in
     * particular) or that are personal message from user to local user.
     */
    private class ServerListener
    extends VariousMessageListenerAdapter
    {
        /**
         * Print out server notices for debugging purposes and for simply
         * keeping track of the connections.
         * 
         * @param msg the server notice
         */
        @Override
        public void onServerNotice(ServerNotice msg)
        {
            System.out.println("NOTICE: " + ((ServerNotice) msg).getText());
        }

        /**
         * Print out server numeric messages for debugging purposes and for
         * simply keeping track of the connection.
         * 
         * @param msg the numeric message
         */
        @Override
        public void onServerNumericMessage(ServerNumericMessage msg)
        {
            System.out.println("NUM MSG: "
                + ((ServerNumericMessage) msg).getNumericCode() + ": "
                + ((ServerNumericMessage) msg).getText());
        }

        /**
         * Print out received errors for debugging purposes and may be for
         * expected errors that can be acted upon.
         * 
         * @param msg the error message
         */
        @Override
        public void onError(ErrorMessage msg)
        {
            System.out.println("ERROR: " + msg.getSource() + ": "
                + msg.getText());
        }
        
        /**
         * Upon receiving a private message from a user, deliver that to a
         * private chat room and create one if it does not exist. We can ignore
         * normal chat rooms, since they each have their own ChatRoomListener
         * for managing chat room operations.
         * 
         * @param msg the private message
         */
        @Override
        public void onUserPrivMessage(UserPrivMsg msg)
        {
            // TODO handle special formatting/color control codes in message
            // (see also channel private messages)

            ChatRoomIrcImpl chatroom = null;
            String user = msg.getSource().getNick();
            String text = msg.getText();
            synchronized (IrcStack.this.joined)
            {
                // Find the chatroom matching the user.
                for (ChatRoomIrcImpl room : IrcStack.this.joined)
                {
                    if (room.isPrivate() == false)
                        continue;

                    if (user.equals(room.getIdentifier()))
                    {
                        chatroom = room;
                        break;
                    }
                }
            }
            if (chatroom == null)
            {
                chatroom = initiatePrivateChatRoom(user);
            }
            deliverReceivedMessageToPrivateChat(chatroom, user, text);
        }

        /**
         * Deliver a private message to the provided chat room.
         * 
         * @param chatroom the chat room
         * @param user the source user
         * @param text the message
         */
        private void deliverReceivedMessageToPrivateChat(ChatRoomIrcImpl chatroom,
            String user, String text)
        {
            ChatRoomMember member = chatroom.getChatRoomMember(user);
            MessageIrcImpl message =
                new MessageIrcImpl(text, "text/plain", "UTF-8", null);
            chatroom.fireMessageReceivedEvent(message, member, new Date(),
                ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED);
        }

        /**
         * Create a private chat room if one does not exist yet.
         * 
         * @param user private chat room for this user
         * @return returns the private chat room
         */
        private ChatRoomIrcImpl initiatePrivateChatRoom(String user)
        {
            ChatRoomIrcImpl chatroom =
                (ChatRoomIrcImpl) IrcStack.this.provider.getMUC()
                    .findRoom(user);
            IrcStack.this.joined.add(chatroom);
            ChatRoomMemberIrcImpl member =
                new ChatRoomMemberIrcImpl(IrcStack.this.provider, chatroom,
                    user, ChatRoomMemberRole.MEMBER);
            chatroom.addChatRoomMember(member.getContactAddress(), member);
            IrcStack.this.provider.getMUC().fireLocalUserPresenceEvent(
                chatroom,
                LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED,
                "Private conversation initiated.");
            return chatroom;
        }
    }<commit_msg>Improvement in syncing disconnect + utilizing map for registering joined channels.<commit_after>/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package net.java.sip.communicator.impl.protocol.irc;

import java.util.*;

import net.java.sip.communicator.impl.protocol.irc.ModeParser.ModeEntry;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;

import com.ircclouds.irc.api.*;
import com.ircclouds.irc.api.domain.*;
import com.ircclouds.irc.api.domain.messages.*;
import com.ircclouds.irc.api.domain.messages.interfaces.*;
import com.ircclouds.irc.api.listeners.*;
import com.ircclouds.irc.api.state.*;

/**
 * An implementation of IRC using the irc-api library.
 * 
 * @author <NAME>
 * 
 * TODO Make irc-api OSGi aware.
 */
public class IrcStack
{
    /**
     * Logger
     */
    private static final Logger LOGGER = Logger.getLogger(IrcStack.class);

    /**
     * Parent provider for IRC
     */
    private final ProtocolProviderServiceIrcImpl provider;

    /**
     * Container for joined channels.
     */
    private final Map<String, ChatRoomIrcImpl> joined = Collections
        .synchronizedMap(new HashMap<String, ChatRoomIrcImpl>());

    /**
     * Server parameters that are set and provided during the connection
     * process.
     */
    private final ServerParameters params;

    /**
     * Instance of the IRC library.
     */
    private IRCApi irc;

    /**
     * Connection state of a successful IRC connection.
     */
    private IIRCState connectionState;
    
    /**
     * The cached channel list.
     * 
     * Contained inside a simple container object in order to lock the container
     * while accessing the contents.
     */
    private final Container<List<String>> channellist =
        new Container<List<String>>(null);

    /**
     * Constructor
     * 
     * @param parentProvider Parent provider
     * @param nick User's nick name
     * @param login User's login name
     * @param version Version
     * @param finger Finger
     */
    public IrcStack(final ProtocolProviderServiceIrcImpl parentProvider,
        final String nick, final String login, final String version,
        final String finger)
    {
        if (parentProvider == null)
        {
            throw new NullPointerException("parentProvider cannot be null");
        }
        this.provider = parentProvider;
        this.params = new IrcStack.ServerParameters(nick, login, finger, null);
    }

    /**
     * Check whether or not a connection is established.
     * 
     * @return true if connected, false otherwise.
     */
    public boolean isConnected()
    {
        return (this.irc != null && this.connectionState != null && this.connectionState
            .isConnected());
    }

    /**
     * Check whether the connection is a secure connection (TLS).
     * 
     * @return true if connection is secure, false otherwise.
     */
    public boolean isSecureConnection()
    {
        return isConnected() && this.connectionState.getServer().isSSL();
    }

    /**
     * Connect to specified host, port, optionally using a password.
     * 
     * @param host IRC server's host name
     * @param port IRC port
     * @param password
     * @param autoNickChange
     * @throws Exception
     */
    public void connect(String host, int port, String password,
        boolean secureConnection, boolean autoNickChange) throws Exception
    {
        if (this.irc != null && this.connectionState != null
            && this.connectionState.isConnected())
            return;

        // Make sure we start with an empty joined-channel list.
        this.joined.clear();

        // A container for storing the exception if connecting fails.
        final Exception[] exceptionContainer = new Exception[1];

        this.irc = new IRCApiImpl(true);
        // FIXME Currently, the secure connection is created by
        // explicitly creating an SSLContext for 'SSL'. According
        // to Ingo (in a mailing list conversation) it is better to
        // use the CertificateService for this. This should be
        // implemented in the irc-api library, though.
        this.params.setServer(new IRCServer(host, port, password,
            secureConnection));
        synchronized (this.irc)
        {
            // register a server listener in order to catch server and
            // cross-/multi-channel messages
            this.irc.addListener(new ServerListener());
            // start connecting to the specified server ...
            // TODO Catch IOException/SocketException in case of early failure
            // in call to connect()
            this.irc.connect(this.params, new Callback<IIRCState>()
            {

                @Override
                public void onSuccess(IIRCState state)
                {
                    synchronized (IrcStack.this.irc)
                    {
                        System.out.println("IRC connected successfully!");
                        IrcStack.this.connectionState = state;
                        IrcStack.this.irc.notifyAll();
                    }
                }

                @Override
                public void onFailure(Exception e)
                {
                    synchronized (IrcStack.this.irc)
                    {
                        System.out.println("IRC connection FAILED! ("
                            + e.getMessage() + ")");
                        exceptionContainer[0] = e;
                        IrcStack.this.connectionState = null;
                        IrcStack.this.irc.notifyAll();
                    }
                }
            });

            try
            {
                // wait while the irc connection is being established ...
                System.out
                    .println("Waiting for the connection to be established ...");
                // TODO Implement connection timeout and a way to recognize that
                // the timeout occurred.
                this.irc.wait();
                if (this.connectionState != null
                    && this.connectionState.isConnected())
                {
                    this.provider
                        .setCurrentRegistrationState(RegistrationState.REGISTERED);
                }
                else
                {
                    this.provider
                        .setCurrentRegistrationState(RegistrationState.UNREGISTERED);

                    if (exceptionContainer[0] != null)
                    {
                        // If an exception happens to be available that explains
                        // the connection problem, throw it.
                        throw exceptionContainer[0];
                    }
                }
            }
            catch (InterruptedException e)
            {
                this.provider
                    .setCurrentRegistrationState(RegistrationState.UNREGISTERED);
                throw e;
            }
        }
    }

    /**
     * Disconnect from the IRC server
     */
    public void disconnect()
    {
        if (this.connectionState == null && this.irc == null)
            return;
        
        synchronized(this.joined)
        {
            // Leave all joined channels.
            for (ChatRoomIrcImpl channel : this.joined.values())
            {
                leave(channel);
            }
        }
        synchronized(this.irc)
        {
            // Disconnect and clean up
            this.irc.disconnect();
            this.irc = null;
            this.connectionState = null;
        }
        this.provider
            .setCurrentRegistrationState(RegistrationState.UNREGISTERED);
    }

    /**
     * Dispose
     */
    public void dispose()
    {
        disconnect();
    }

    /**
     * Get the nick name of the user.
     * 
     * @return Returns either the acting nick if a connection is established or
     *         the configured nick.
     */
    public String getNick()
    {
        return (this.connectionState == null) ? this.params.getNickname()
            : this.connectionState.getNickname();
    }

    /**
     * Set the user's new nick name.
     * 
     * @param nick the new nick name
     */
    public void setUserNickname(String nick)
    {
        if (this.connectionState == null)
        {
            this.params.setNickname(nick);
        }
        else
        {
            this.irc.changeNick(nick);
        }
    }

    /**
     * Set the subject of the specified chat room.
     * 
     * @param chatroom The chat room for which to set the subject.
     * @param subject The subject.
     */
    public void setSubject(ChatRoomIrcImpl chatroom, String subject)
    {
        if (isConnected() == false)
            throw new IllegalStateException(
                "Please connect to an IRC server first.");
        if (chatroom == null)
            throw new IllegalArgumentException("Cannot have a null chatroom");
        this.irc
            .changeTopic(chatroom.getName(), subject == null ? "" : subject);
    }

    /**
     * Check whether the user has joined a particular chat room.
     * 
     * @param chatroom Chat room to check for.
     * @return Returns true in case the user is already joined, or false if the
     *         user has not joined.
     */
    public boolean isJoined(ChatRoomIrcImpl chatroom)
    {
        return this.joined.containsKey(chatroom.getIdentifier());
    }

    /**
     * Get a list of channels available on the IRC server.
     * 
     * @return List of available channels.
     */
    public List<String> getServerChatRoomList()
    {
        LOGGER.trace("Start retrieve server chat room list.");
        // FIXME Currently, not using an API library method for listing
        // channels, since it isn't available.
        synchronized (this.channellist)
        {
            if (this.channellist.instance == null)
            {
                List<String> list = new LinkedList<String>();
                synchronized (list)
                {
                    try
                    {
                        this.irc.addListener(new ChannelListListener(this.irc,
                            list));
                        this.irc.rawMessage("LIST");
                        LOGGER.trace("Start waiting for list ...");
                        list.wait();
                        LOGGER.trace("Done waiting for list.");
                    }
                    catch (InterruptedException e)
                    {
                        LOGGER.trace("INTERRUPTED while waiting for list.");
                        e.printStackTrace();
                    }
                }
                this.channellist.instance = list;
            }
            LOGGER.trace("Finished retrieve server chat room list.");
            return Collections.unmodifiableList(this.channellist.instance);
        }
    }

    /**
     * Join a particular chat room.
     * 
     * @param chatroom Chat room to join.
     */
    public void join(ChatRoomIrcImpl chatroom)
    {
        join(chatroom, "");
    }

    /**
     * Join a particular chat room.
     * 
     * Issue a join channel IRC operation and wait for the join operation to
     * complete (either successfully or failing).
     * 
     * @param chatroom The chatroom to join.
     * @param password Optionally, a password that may be required for some
     *            channels.
     */
    public void join(final ChatRoomIrcImpl chatroom, final String password)
    {
        if (isConnected() == false)
            throw new IllegalStateException(
                "Please connect to an IRC server first");
        if (chatroom == null)
            throw new IllegalArgumentException("chatroom cannot be null");
        if (password == null)
            throw new IllegalArgumentException("password cannot be null");
        if (this.joined.containsKey(chatroom.getIdentifier()) || chatroom.isPrivate())
        {
            // If we already joined this particular chatroom or if it is a
            // private chat room (i.e. message from one user to another), no
            // further action is required.
            return;
        }

        LOGGER.trace("Start joining channel " + chatroom.getIdentifier());
        final Object joinSignal = new Object();
        synchronized (joinSignal)
        {
            try
            {
                LOGGER
                    .trace("Issue join channel command to IRC library and wait for join operation to complete (un)successfully.");
                // TODO Refactor this ridiculous nesting of functions and
                // classes.
                this.irc.joinChannel(chatroom.getIdentifier(), password,
                    new Callback<IRCChannel>()
                    {

                        @Override
                        public void onSuccess(IRCChannel channel)
                        {
                            synchronized (joinSignal)
                            {
                                try
                                {
                                    // TODO Handle forward to another channel
                                    // (470) channel name change.
                                    // (Testable on irc.freenode.net#linux,
                                    // forwards to ##linux)
                                    // Currently drops into an infinite wait
                                    // because the callback is never
                                    // executed. Not sure how to catch/detect
                                    // this forward operation and act
                                    // appropriately. Seems reasonable to just
                                    // expect the same callback, but
                                    // with different channel information.
                                    IrcStack.this.joined.put(
                                        chatroom.getIdentifier(), chatroom);
                                    IrcStack.this.irc
                                        .addListener(new ChatRoomListener(
                                            chatroom));
                                    IRCTopic topic = channel.getTopic();
                                    chatroom.updateSubject(topic.getValue());

                                    for (IRCUser user : channel.getUsers())
                                    {
                                        ChatRoomMemberRole role =
                                            ChatRoomMemberRole.SILENT_MEMBER;
                                        Set<IRCUserStatus> statuses =
                                            channel.getStatusesForUser(user);
                                        for (IRCUserStatus status : statuses)
                                        {
                                            role =
                                                convertMemberMode(status
                                                    .getChanModeType()
                                                    .charValue());
                                        }

                                        if (IrcStack.this.getNick().equals(
                                            user.getNick()))
                                        {
                                            chatroom.prepUserRole(role);
                                        }
                                        else
                                        {
                                            ChatRoomMemberIrcImpl member =
                                                new ChatRoomMemberIrcImpl(
                                                    IrcStack.this.provider,
                                                    chatroom, user.getNick(),
                                                    role);
                                            chatroom.addChatRoomMember(
                                                member.getContactAddress(),
                                                member);
                                        }
                                    }
                                }
                                finally
                                {
                                    // In any case, issue the local user
                                    // presence, since the irc library notified
                                    // us of a successful join. We should wait
                                    // as long as possible though. First we need
                                    // to fill the list of chat room members and
                                    // other chat room properties.
                                    IrcStack.this.provider
                                        .getMUC()
                                        .fireLocalUserPresenceEvent(
                                            chatroom,
                                            LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED,
                                            null);
                                    LOGGER
                                        .trace("Finished successful join callback. Waking up original thread.");
                                    // Notify waiting threads of finished
                                    // execution.
                                    joinSignal.notifyAll();
                                }
                            }
                        }

                        @Override
                        public void onFailure(Exception e)
                        {
                            // TODO how should we communicate a failed attempt
                            // at joining the channel?
                            synchronized (joinSignal)
                            {
                                try
                                {
                                    MessageIrcImpl message =
                                        new MessageIrcImpl(
                                            "Failed to join channel "
                                                + chatroom.getIdentifier()
                                                + " (" + e.getMessage() + ")",
                                            "text/plain", "UTF-8",
                                            "Failed to join");
                                    chatroom
                                        .fireMessageReceivedEvent(
                                            message,
                                            null,
                                            new Date(),
                                            MessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED);
                                }
                                finally
                                {
                                    LOGGER
                                        .trace("Finished unsuccessful join callback. Waking up original thread.");
                                    // Notify waiting threads of finished
                                    // execution
                                    joinSignal.notifyAll();
                                }
                            }
                        }
                    });
                // Wait until async channel join operation has finished.
                joinSignal.wait();
                LOGGER
                    .trace("Finished waiting for join operation to complete.");
                // TODO How to handle 471 (+l): Channel is full (reached set
                // user limit)?
                // TODO How to handle 480 (+j): Channel throttle exceeded?
            }
            catch (InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

    /**
     * Part from a joined chat room.
     * 
     * @param chatroom The chat room to part from.
     */
    public void leave(ChatRoomIrcImpl chatroom)
    {
        if (chatroom.isPrivate() == false)
        {
            // You only actually join non-private chat rooms, so only these ones
            // need to be left.
            leave(chatroom.getIdentifier());
        }
    }

    /**
     * Part from a joined chat room.
     * 
     * @param chatRoomName The chat room to part from.
     */
    private void leave(String chatRoomName)
    {
        this.irc.leaveChannel(chatRoomName);
    }

    public void banParticipant(ChatRoomIrcImpl chatroom, ChatRoomMember member,
        String reason)
    {
        // TODO Implement this.
    }

    /**
     * Kick channel member.
     * 
     * @param chatroom channel to kick from
     * @param member member to kick
     * @param reason kick message to deliver
     */
    public void kickParticipant(ChatRoomIrcImpl chatroom,
        ChatRoomMember member, String reason)
    {
        this.irc.kick(chatroom.getIdentifier(), member.getContactAddress(),
            reason);
    }

    /**
     * Issue invite command to IRC server.
     * 
     * @param memberId member to invite
     * @param chatroom channel to invite to
     */
    public void invite(String memberId, ChatRoomIrcImpl chatroom)
    {
        this.irc.rawMessage("INVITE " + memberId + " "
            + chatroom.getIdentifier());
    }

    /**
     * Send a command to the IRC server.
     * @param chatroom the chat room
     * @param command the command message
     */
    public void command(ChatRoomIrcImpl chatroom, String command)
    {
        String target;
        if (command.toLowerCase().startsWith("/msg "))
        {
            command = command.substring(5);
            int endOfNick = command.indexOf(' ');
            if (endOfNick == -1)
            {
                throw new IllegalArgumentException("Invalid private message format. Message was not sent.");
            }
            target = command.substring(0,  endOfNick);
            command = command.substring(endOfNick+1);
        }
        else
        {
            target = chatroom.getIdentifier();
        }
        this.irc.message(target, command);
    }

    /**
     * Send an IRC message.
     * 
     * @param chatroom The chat room to send the message to.
     * @param message The message to send.
     */
    public void message(ChatRoomIrcImpl chatroom, String message)
    {
        String target = chatroom.getIdentifier();
        this.irc.message(target, message);
    }

    /**
     * Convert a member mode character to a ChatRoomMemberRole instance.
     * 
     * @param modeSymbol The member mode character.
     * @return Return the instance of ChatRoomMemberRole corresponding to the
     *         member mode character.
     */
    private static ChatRoomMemberRole convertMemberMode(char modeSymbol)
    {
        return Mode.bySymbol(modeSymbol).getRole();
    }

    /**
     * A listener for server-level messages (any messages that are related to the
     * server, the connection, that are not related to any chatroom in
     * particular) or that are personal message from user to local user.
     */
    private class ServerListener
    extends VariousMessageListenerAdapter
    {
        /**
         * Print out server notices for debugging purposes and for simply
         * keeping track of the connections.
         * 
         * @param msg the server notice
         */
        @Override
        public void onServerNotice(ServerNotice msg)
        {
            System.out.println("NOTICE: " + ((ServerNotice) msg).getText());
        }

        /**
         * Print out server numeric messages for debugging purposes and for
         * simply keeping track of the connection.
         * 
         * @param msg the numeric message
         */
        @Override
        public void onServerNumericMessage(ServerNumericMessage msg)
        {
            System.out.println("NUM MSG: "
                + ((ServerNumericMessage) msg).getNumericCode() + ": "
                + ((ServerNumericMessage) msg).getText());
        }

        /**
         * Print out received errors for debugging purposes and may be for
         * expected errors that can be acted upon.
         * 
         * @param msg the error message
         */
        @Override
        public void onError(ErrorMessage msg)
        {
            System.out.println("ERROR: " + msg.getSource() + ": "
                + msg.getText());
        }
        
        /**
         * Upon receiving a private message from a user, deliver that to a
         * private chat room and create one if it does not exist. We can ignore
         * normal chat rooms, since they each have their own ChatRoomListener
         * for managing chat room operations.
         * 
         * @param msg the private message
         */
        @Override
        public void onUserPrivMessage(UserPrivMsg msg)
        {
            // TODO handle special formatting/color control codes in message
            // (see also channel private messages)

            ChatRoomIrcImpl chatroom = null;
            String user = msg.getSource().getNick();
            String text = msg.getText();
            chatroom = IrcStack.this.joined.get(user);
            if (chatroom == null)
            {
                chatroom = initiatePrivateChatRoom(user);
            }
            deliverReceivedMessageToPrivateChat(chatroom, user, text);
        }

        /**
         * Deliver a private message to the provided chat room.
         * 
         * @param chatroom the chat room
         * @param user the source user
         * @param text the message
         */
        private void deliverReceivedMessageToPrivateChat(ChatRoomIrcImpl chatroom,
            String user, String text)
        {
            ChatRoomMember member = chatroom.getChatRoomMember(user);
            MessageIrcImpl message =
                new MessageIrcImpl(text, "text/plain", "UTF-8", null);
            chatroom.fireMessageReceivedEvent(message, member, new Date(),
                ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED);
        }

        /**
         * Create a private chat room if one does not exist yet.
         * 
         * @param user private chat room for this user
         * @return returns the private chat room
         */
        private ChatRoomIrcImpl initiatePrivateChatRoom(String user)
        {
            ChatRoomIrcImpl chatroom =
                (ChatRoomIrcImpl) IrcStack.this.provider.getMUC()
                    .findRoom(user);
            IrcStack.this.joined.put(chatroom.getIdentifier(), chatroom);
            ChatRoomMemberIrcImpl member =
                new ChatRoomMemberIrcImpl(IrcStack.this.provider, chatroom,
                    user, ChatRoomMemberRole.MEMBER);
            chatroom.addChatRoomMember(member.getContactAddress(), member);
            IrcStack.this.provider.getMUC().fireLocalUserPresenceEvent(
                chatroom,
                LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED,
                "Private conversation initiated.");
            return chatroom;
        }
    }
{'fraction_non_alphanumeric': 0.07414657348798068, 'fraction_numerical': 0.0006951812959642897, 'mean_word_length': 1.1982145011460972, 'pattern_counts': {'":': 4, '<': 29, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 235, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3521170', 'n_tokens_mistral': 12348, 'n_tokens_neox': 11719, 'n_words': 3556}