diff options
author | Danny van Heumen <danny@dannyvanheumen.nl> | 2014-09-28 22:28:21 +0200 |
---|---|---|
committer | Danny van Heumen <danny@dannyvanheumen.nl> | 2014-10-28 22:33:31 +0100 |
commit | 6be6df2777253355f10cbd6c2f09362bf5d2966f (patch) | |
tree | 7bd725bea1a0b7dc888812e711708c235a87fd4f | |
parent | 518995debd939233c8b275d38dc6df735bf2ec40 (diff) | |
download | jitsi-6be6df2777253355f10cbd6c2f09362bf5d2966f.zip jitsi-6be6df2777253355f10cbd6c2f09362bf5d2966f.tar.gz jitsi-6be6df2777253355f10cbd6c2f09362bf5d2966f.tar.bz2 |
Restructured and extract IrcConnection type for better separation of
stack and connection.
This restructuring extracts all the methods that are related to an
established connection such that there is a better separation of
concerns between IRC stack state and the state for an individual
connection.
This way it is even possible to set null connection even before the
connection, its listeners, and its threads are completely disposed of.
This also fixes the bug with auto-join behaviour that occurs because of
multi-threading and bad (i.e. no) synchronization on the IRCApi
instance.
8 files changed, 2611 insertions, 2413 deletions
diff --git a/src/net/java/sip/communicator/impl/protocol/irc/ChatRoomIrcImpl.java b/src/net/java/sip/communicator/impl/protocol/irc/ChatRoomIrcImpl.java index d3bd2bc..1e35908 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/ChatRoomIrcImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/ChatRoomIrcImpl.java @@ -158,9 +158,10 @@ public class ChatRoomIrcImpl throw new IllegalArgumentException("parentProvider cannot be null"); } this.parentProvider = parentProvider; + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); this.chatRoomName = - verifyName(this.parentProvider.getIrcStack().getChannelTypes(), - chatRoomName); + verifyName(connection.getChannelTypes(), chatRoomName); this.isSystem = isSystem; } @@ -185,7 +186,7 @@ public class ChatRoomIrcImpl // listed as a channel type. if (channelTypes.contains(prefix) || prefix == DEFAULT_CHANNEL_PREFIX) { - for (char c : IrcStack.SPECIAL_CHARACTERS) + for (char c : IrcConnection.SPECIAL_CHARACTERS) { if (name.contains("" + c)) { @@ -307,20 +308,22 @@ public class ChatRoomIrcImpl */ public void join() throws OperationFailedException { - if (!parentProvider.getIrcStack().isConnected()) + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + if (connection == null || !connection.isConnected()) { throw new OperationFailedException( "We are currently not connected to the server.", OperationFailedException.NETWORK_FAILURE); } - if (parentProvider.getIrcStack().isJoined(this)) + if (connection.isJoined(this)) { throw new OperationFailedException("Channel is already joined.", OperationFailedException.SUBSCRIPTION_ALREADY_EXISTS); } - parentProvider.getIrcStack().join(this); + connection.join(this); } /** @@ -334,7 +337,9 @@ public class ChatRoomIrcImpl */ public void join(final byte[] password) throws OperationFailedException { - parentProvider.getIrcStack().join(this, password.toString()); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.join(this, password.toString()); } /** @@ -387,7 +392,9 @@ public class ChatRoomIrcImpl */ public boolean isJoined() { - return parentProvider.getIrcStack().isJoined(this); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + return connection.isJoined(this); } /** @@ -399,7 +406,9 @@ public class ChatRoomIrcImpl */ public void leave() { - this.parentProvider.getIrcStack().leave(this); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.leave(this); this.chatRoomMembers.clear(); } @@ -427,8 +436,9 @@ public class ChatRoomIrcImpl public void banParticipant(final ChatRoomMember chatRoomMember, final String reason) throws OperationFailedException { - this.parentProvider.getIrcStack().banParticipant(this, - chatRoomMember, reason); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.banParticipant(this, chatRoomMember, reason); } /** @@ -442,8 +452,9 @@ public class ChatRoomIrcImpl public void kickParticipant(final ChatRoomMember chatRoomMember, final String reason) throws OperationFailedException { - this.parentProvider.getIrcStack().kickParticipant(this, - chatRoomMember, reason); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.kickParticipant(this, chatRoomMember, reason); } /** @@ -640,7 +651,9 @@ public class ChatRoomIrcImpl { try { - parentProvider.getIrcStack().setSubject(this, subject); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.setSubject(this, subject); } catch (RuntimeException e) { @@ -665,7 +678,9 @@ public class ChatRoomIrcImpl { // User's nick name is determined by the server connection, not the // individual chat rooms. - return parentProvider.getIrcStack().getNick(); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + return connection.getNick(); } /** @@ -800,7 +815,9 @@ public class ChatRoomIrcImpl // TODO Check if channel status is invite-only (+i). If this is the // case, user has to be channel operator in order to be able to invite // some-one. - parentProvider.getIrcStack().invite(userAddress, this); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.invite(userAddress, this); } /** @@ -894,13 +911,15 @@ public class ChatRoomIrcImpl continue; } + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); if (((MessageIrcImpl) message).isCommand()) { - parentProvider.getIrcStack().command(this, messagePortion); + connection.command(this, messagePortion); } else { - parentProvider.getIrcStack().message(this, messagePortion); + connection.message(this, messagePortion); } this.fireMessageDeliveredEvent(new MessageIrcImpl(messagePortion, @@ -1322,7 +1341,9 @@ public class ChatRoomIrcImpl @Override public void grantAdmin(final String address) { - this.parentProvider.getIrcStack().grant(this, address, Mode.OPERATOR); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.grant(this, address, Mode.OPERATOR); } /** @@ -1333,7 +1354,9 @@ public class ChatRoomIrcImpl public void grantMembership(final String address) { // TODO currently Voice == Membership. - this.parentProvider.getIrcStack().grant(this, address, Mode.VOICE); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.grant(this, address, Mode.VOICE); } /** @@ -1343,7 +1366,9 @@ public class ChatRoomIrcImpl @Override public void grantModerator(final String address) { - this.parentProvider.getIrcStack().grant(this, address, Mode.HALFOP); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.grant(this, address, Mode.HALFOP); } /** @@ -1353,7 +1378,9 @@ public class ChatRoomIrcImpl @Override public void grantOwnership(final String address) { - this.parentProvider.getIrcStack().grant(this, address, Mode.OWNER); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.grant(this, address, Mode.OWNER); } /** @@ -1364,7 +1391,9 @@ public class ChatRoomIrcImpl public void grantVoice(final String address) { // TODO currently Voice == Membership. - this.parentProvider.getIrcStack().grant(this, address, Mode.VOICE); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.grant(this, address, Mode.VOICE); } /** @@ -1374,7 +1403,9 @@ public class ChatRoomIrcImpl @Override public void revokeAdmin(final String address) { - this.parentProvider.getIrcStack().revoke(this, address, Mode.OPERATOR); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.revoke(this, address, Mode.OPERATOR); } /** @@ -1387,7 +1418,9 @@ public class ChatRoomIrcImpl @Override public void revokeMembership(final String address) { - this.parentProvider.getIrcStack().revoke(this, address, Mode.VOICE); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.revoke(this, address, Mode.VOICE); } /** @@ -1398,7 +1431,9 @@ public class ChatRoomIrcImpl @Override public void revokeModerator(final String address) { - this.parentProvider.getIrcStack().revoke(this, address, Mode.HALFOP); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.revoke(this, address, Mode.HALFOP); } /** @@ -1409,7 +1444,9 @@ public class ChatRoomIrcImpl @Override public void revokeOwnership(final String address) { - this.parentProvider.getIrcStack().revoke(this, address, Mode.OWNER); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.revoke(this, address, Mode.OWNER); } /** @@ -1419,7 +1456,9 @@ public class ChatRoomIrcImpl @Override public void revokeVoice(final String address) { - this.parentProvider.getIrcStack().revoke(this, address, Mode.VOICE); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + connection.revoke(this, address, Mode.VOICE); } /** diff --git a/src/net/java/sip/communicator/impl/protocol/irc/IrcConnection.java b/src/net/java/sip/communicator/impl/protocol/irc/IrcConnection.java new file mode 100644 index 0000000..c9f9f35 --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/irc/IrcConnection.java @@ -0,0 +1,2392 @@ +/* + * 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.io.*; +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.*; + +/** + * IRC Connection. + * + * TODO Do we need to cancel any join channel operations still in progress? + * + * Common IRC network facilities: + * 1. NickServ - nick related services + * 2. ChanServ - channel related services + * 3. MemoServ - message relaying services + * + * @author Danny van Heumen + */ +public class IrcConnection +{ + /** + * TODO In the far far future ... + * + * <p> + * Some of the less pressing features that may one day be useful ... + * </p> + * + * <pre> + * - Handle 404 ERR_CANNOTSENDTOCHAN in case of +n channel mode and not + * joined to the channel where you send a message to. + * </pre> + */ + + /** + * Logger. + */ + private static final Logger LOGGER = Logger.getLogger(IrcConnection.class); + + /** + * Maximum message size for IRC messages given the spec specifies a buffer + * of 512 bytes. The command ending (CRLF) takes up 2 bytes. + */ + private static final int IRC_PROTOCOL_MAXIMUM_MESSAGE_SIZE = 510; + + /** + * Clean-up delay. The clean up task clears any remaining chat room list + * cache. Since there's no pointing in timing it exactly, delay the clean up + * until after expiration. + */ + private static final long CACHE_CLEAN_UP_DELAY = 1000L; + + /** + * Ratio of milliseconds to nanoseconds for conversions. + */ + private static final long RATIO_MILLISECONDS_TO_NANOSECONDS = 1000000L; + + /** + * Expiration time for chat room list cache. + */ + private static final long CHAT_ROOM_LIST_CACHE_EXPIRATION = 60000000000L; + + /** + * Set of characters with special meanings for IRC, such as: ',' used as + * separator of list of items (channels, nicks, etc.), ' ' (space) separator + * of command parameters, etc. + */ + public static final Set<Character> SPECIAL_CHARACTERS; + + /** + * Initialize set of special characters. + */ + static { + HashSet<Character> specials = new HashSet<Character>(); + specials.add('\0'); + specials.add('\n'); + specials.add('\r'); + specials.add(' '); + specials.add(','); + SPECIAL_CHARACTERS = Collections.unmodifiableSet(specials); + } + + /** + * Instance of the protocol provider service. + */ + private final ProtocolProviderServiceIrcImpl provider; + + /** + * Instance of IRC Api. + */ + private final IRCApi irc; + + /** + * Connection state of a successful IRC connection. + */ + private final IIRCState connectionState; + + /** + * Manager component that manages current IRC presence. + */ + private PresenceManager presence = null; + + /** + * The local user's identity as it will be used in server-client + * communication for sent messages. + */ + private Identity identity; + + /** + * Container for joined channels. + * + * There are two different cases: + * + * <pre> + * - null value: joining is initiated but still in progress. + * - non-null value: joining is finished, chat room instance is available. + * </pre> + */ + private final Map<String, ChatRoomIrcImpl> joined = Collections + .synchronizedMap(new HashMap<String, ChatRoomIrcImpl>()); + + /** + * The cached channel list. + * + * Contained inside a simple container object in order to lock the container + * while accessing the contents. + * + * TODO move channellist operations to different class + */ + private final Container<List<String>> channellist = + new Container<List<String>>(null); + + /** + * Constructor. + * + * @param provider ProtocolProviderService instance + * @param params connection parameters + * @param irc IRC api instance + * @throws Exception Throws IOException in case of connection problems. + */ + public IrcConnection(final ProtocolProviderServiceIrcImpl provider, + final IServerParameters params, final IRCApi irc) + throws Exception + { + if (provider == null) + { + throw new IllegalArgumentException("provider cannot be null"); + } + this.provider = provider; + if (irc == null) + { + throw new IllegalArgumentException("irc instance cannot be null"); + } + // Install a listener for everything that is not directly related to a + // specific chat room or operation. + irc.addListener(new ServerListener(irc)); + this.irc = irc; + this.connectionState = connectSynchronized(params, irc); + + // instantiate presence manager for the connection + this.presence = + new PresenceManager(irc, this.connectionState, + this.provider.getPersistentPresence()); + + queryIdentity(); + + // TODO Read IRC network capabilities based on RPL_ISUPPORT + // (005) replies if available. This information should be + // available in irc-api if possible. + } + + /** + * Perform synchronized connect operation. + * + * @param irc IRC Api instance + * @throws Exception exception thrown when connect fails + */ + private IIRCState connectSynchronized(final IServerParameters params, + final IRCApi irc) throws Exception + { + final Result<IIRCState, Exception> result = + new Result<IIRCState, Exception>(); + synchronized (result) + { + // start connecting to the specified server ... + irc.connect(params, new Callback<IIRCState>() + { + + @Override + public void onSuccess(final IIRCState state) + { + synchronized (result) + { + LOGGER.trace("IRC connected successfully!"); + result.setDone(state); + result.notifyAll(); + } + } + + @Override + public void onFailure(final Exception e) + { + synchronized (result) + { + LOGGER.trace("IRC connection FAILED!", e); + result.setDone(e); + result.notifyAll(); + } + } + }); + + this.provider + .setCurrentRegistrationState(RegistrationState.REGISTERING); + + while (!result.isDone()) + { + LOGGER.trace("Waiting for the connection to be " + + "established ..."); + result.wait(); + } + } + + // TODO Implement connection timeout and a way to recognize that + // the timeout occurred. + + final Exception e = result.getException(); + if (e != null) + { + throw new IOException(e); + } + + final IIRCState state = result.getValue(); + if (state == null) + { + throw new IOException( + "Failed to connect to IRC server: connection state is null"); + } + + return state; + } + + /** + * Issue WHOIS query to discover identity as seen by the server. + */ + private void queryIdentity() + { + // TODO Install temporary whois listener that handles the result. + this.irc.rawMessage("WHOIS " + this.connectionState.getNickname()); + } + + /** + * Get the current identity string, based on nick, user and host of local + * user. + * + * @return returns identity string + */ + public String getIdentityString() + { + final String currentNick = this.connectionState.getNickname(); + return this.identity.getIdentityString(currentNick); + } + + /** + * Check whether current state is away or online. + * + * @return returns true if away, or false if online + */ + public boolean isAway() + { + return isConnected() && this.presence.isAway(); + } + + /** + * Set or unset away message. In case the awayMessage is null the away + * message will be disabled and as a consequence the away-status is removed. + * + * @param away away status, <tt>true</tt> for away, <tt>false</tt> for + * available + * @param awayMessage the away message to set, or null to remove away-status + */ + public void away(final boolean away, final String awayMessage) + { + if (!isConnected()) + { + throw new IllegalStateException("Not connected to an IRC server."); + } + this.presence.away(away, awayMessage); + } + + /** + * Check whether or not a connection is established. + * + * @return true if connected, false otherwise. + */ + public boolean isConnected() + { + return (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(); + } + + /** + * Disconnect. + */ + void disconnect() + { + try + { + synchronized (this.irc) + { + this.irc.disconnect(); + } + } + catch (RuntimeException e) + { + // Disconnect might throw ChannelClosedException. Shouldn't be a + // problem, but for now lets log it just to be sure. + LOGGER.debug("exception occurred while disconnecting", e); + } + } + + /** + * Get a set of channel type indicators. + * + * @return returns set of channel type indicators. + */ + public Set<Character> getChannelTypes() + { + if (!isConnected()) + { + throw new IllegalStateException("not connected to IRC server"); + } + return this.connectionState.getServerOptions().getChanTypes(); + } + + /** + * 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.getNickname(); + } + + /** + * 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(final ChatRoomIrcImpl chatroom, final String subject) + { + if (!isConnected()) + { + throw new IllegalStateException( + "Please connect to an IRC server first."); + } + if (chatroom == null) + { + throw new IllegalArgumentException("Cannot have a null chatroom"); + } + LOGGER.trace("Setting chat room topic to '" + subject + "'"); + synchronized (this.irc) + { + this.irc.changeTopic(chatroom.getIdentifier(), 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(final ChatRoomIrcImpl chatroom) + { + return this.joined.get(chatroom.getIdentifier()) != null; + } + + /** + * 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."); + if (!isConnected()) + { + throw new IllegalStateException("Not connected to an IRC server."); + } + + // TODO Currently, not using an API library method for listing + // channels, since it isn't available. + synchronized (this.channellist) + { + List<String> list = + this.channellist.get(CHAT_ROOM_LIST_CACHE_EXPIRATION); + if (list == null) + { + LOGGER + .trace("Chat room list null or outdated. Start retrieving " + + "new chat room list."); + Result<List<String>, Exception> listSignal = + new Result<List<String>, Exception>( + new LinkedList<String>()); + synchronized (listSignal) + { + try + { + this.irc.addListener( + new ChannelListListener(this.irc, listSignal)); + synchronized (this.irc) + { + this.irc.rawMessage("LIST"); + } + while (!listSignal.isDone()) + { + LOGGER.trace("Waiting for list ..."); + listSignal.wait(); + } + LOGGER.trace("Done waiting for list."); + } + catch (InterruptedException e) + { + LOGGER.warn("INTERRUPTED while waiting for list.", e); + } + } + list = listSignal.getValue(); + this.channellist.set(list); + LOGGER.trace("Finished retrieving server chat room list."); + + // Set timer to clean up the cache after use, since otherwise it + // could stay in memory for a long time. + createCleanUpJob(this.channellist); + } + else + { + LOGGER.trace("Using cached list of server chat rooms."); + } + return Collections.unmodifiableList(list); + } + } + + /** + * Create a clean up job that checks the container after the cache has + * expired. If the container is still populated, then remove it. This clean + * up makes sure that there are no references left to an otherwise useless + * list of channels. + * + * @param channellist the container carrying the list of channel names + */ + private static void createCleanUpJob( + final Container<List<String>> channellist) + { + final Timer cleanUpJob = new Timer(); + final long timestamp = channellist.getTimestamp(); + cleanUpJob.schedule(new ChannelListCacheCleanUpTask(channellist, + timestamp), CHAT_ROOM_LIST_CACHE_EXPIRATION + / RATIO_MILLISECONDS_TO_NANOSECONDS + CACHE_CLEAN_UP_DELAY); + } + + /** + * Join a particular chat room. + * + * @param chatroom Chat room to join. + * @throws OperationFailedException failed to join the chat room + */ + public void join(final ChatRoomIrcImpl chatroom) + throws OperationFailedException + { + 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. + * @throws OperationFailedException failed to join the chat room + */ + public void join(final ChatRoomIrcImpl chatroom, final String password) + throws OperationFailedException + { + if (!isConnected()) + { + 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"); + } + + final String chatRoomId = chatroom.getIdentifier(); + if (this.joined.containsKey(chatRoomId)) + { + // If we already joined this particular chatroom, no further action + // is required. + return; + } + + LOGGER.trace("Start joining channel " + chatRoomId); + final Result<Object, Exception> joinSignal = + new Result<Object, Exception>(); + synchronized (joinSignal) + { + LOGGER + .trace("Issue join channel command to IRC library and wait for" + + " join operation to complete (un)successfully."); + + this.joined.put(chatRoomId, null); + synchronized (this.irc) + { + // TODO Refactor this ridiculous nesting of functions and + // classes. + this.irc.joinChannel(chatRoomId, password, + new Callback<IRCChannel>() + { + + @Override + public void onSuccess(final IRCChannel channel) + { + if (LOGGER.isTraceEnabled()) + { + LOGGER + .trace("Started callback for successful " + + "join of channel '" + + chatroom.getIdentifier() + "'."); + } + boolean isRequestedChatRoom = + channel.getName().equalsIgnoreCase(chatRoomId); + synchronized (joinSignal) + { + if (!isRequestedChatRoom) + { + // We joined another chat room than the one + // we + // requested initially. + if (LOGGER.isTraceEnabled()) + { + LOGGER.trace("Callback for successful " + + "join finished prematurely " + + "since we got forwarded from " + + "'" + + chatRoomId + + "' to '" + + channel.getName() + + "'. Joining of forwarded channel " + + "gets handled by Server Listener " + + "since that channel was not " + + "announced."); + } + // Remove original chat room id from + // joined-list + // since we aren't actually attempting to + // join + // this room anymore. + IrcConnection.this.joined + .remove(chatRoomId); + IrcConnection.this.provider + .getMUC() + .fireLocalUserPresenceEvent( + chatroom, + LocalUserChatRoomPresenceChangeEvent + .LOCAL_USER_JOIN_FAILED, + "We got forwarded to channel '" + + channel.getName() + "'."); + // Notify waiting threads of finished + // execution. + joinSignal.setDone(); + joinSignal.notifyAll(); + // The channel that we were forwarded to + // will be + // handled by the Server Listener, since the + // channel join was unannounced, and we are + // done + // here. + return; + } + + try + { + IrcConnection.this.joined.put(chatRoomId, + chatroom); + IrcConnection.this.irc + .addListener(new ChatRoomListener( + IrcConnection.this.irc, chatroom)); + prepareChatRoom(chatroom, channel); + } + 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. + IrcConnection.this.provider + .getMUC() + .fireLocalUserPresenceEvent( + chatroom, + LocalUserChatRoomPresenceChangeEvent + .LOCAL_USER_JOINED, + null); + if (LOGGER.isTraceEnabled()) + { + LOGGER + .trace("Finished successful join " + + "callback for channel '" + + chatRoomId + + "'. Waking up original " + + "thread."); + } + // Notify waiting threads of finished + // execution. + joinSignal.setDone(); + joinSignal.notifyAll(); + } + } + } + + @Override + public void onFailure(final Exception e) + { + LOGGER + .trace("Started callback for failed attempt to " + + "join channel '" + chatRoomId + "'."); + synchronized (joinSignal) + { + try + { + IrcConnection.this.joined + .remove(chatRoomId); + IrcConnection.this.provider + .getMUC() + .fireLocalUserPresenceEvent( + chatroom, + LocalUserChatRoomPresenceChangeEvent + .LOCAL_USER_JOIN_FAILED, + e.getMessage()); + } + finally + { + if (LOGGER.isTraceEnabled()) + { + LOGGER + .trace("Finished callback for " + + "failed attempt to join " + + "channel '" + + chatRoomId + + "'. Waking up original " + + "thread."); + } + // Notify waiting threads of finished + // execution + joinSignal.setDone(e); + joinSignal.notifyAll(); + } + } + } + }); + } + + try + { + while (!joinSignal.isDone()) + { + LOGGER.trace("Waiting for channel join message ..."); + // Wait until async channel join operation has finished. + joinSignal.wait(); + } + + LOGGER + .trace("Finished waiting for join operation for channel '" + + chatroom.getIdentifier() + "' to complete."); + // TODO How to handle 480 (+j): Channel throttle exceeded? + } + catch (InterruptedException e) + { + LOGGER.error("Wait for join operation was interrupted.", e); + throw new OperationFailedException(e.getMessage(), + OperationFailedException.INTERNAL_ERROR, e); + } + } + } + + /** + * Part from a joined chat room. + * + * @param chatroom The chat room to part from. + */ + public void leave(final ChatRoomIrcImpl chatroom) + { + LOGGER.trace("Leaving chat room '" + chatroom.getIdentifier() + "'."); + leave(chatroom.getIdentifier()); + } + + /** + * Part from a joined chat room. + * + * @param chatRoomName The chat room to part from. + */ + private void leave(final String chatRoomName) + { + if (!isConnected()) + { + throw new IllegalStateException("Not connected to an IRC server."); + } + + try + { + synchronized (this.irc) + { + this.irc.leaveChannel(chatRoomName); + } + } + catch (ApiException e) + { + LOGGER.warn("exception occurred while leaving channel", e); + } + } + + /** + * Ban chat room member. + * + * @param chatroom chat room to ban from + * @param member member to ban + * @param reason reason for banning + * @throws OperationFailedException throws operation failed in case of + * trouble. + */ + public void banParticipant(final ChatRoomIrcImpl chatroom, + final ChatRoomMember member, final String reason) + throws OperationFailedException + { + // TODO Implement banParticipant. + throw new OperationFailedException("Not implemented yet.", + OperationFailedException.NOT_SUPPORTED_OPERATION); + } + + /** + * Kick channel member. + * + * @param chatroom channel to kick from + * @param member member to kick + * @param reason kick message to deliver + */ + public void kickParticipant(final ChatRoomIrcImpl chatroom, + final ChatRoomMember member, final String reason) + { + if (!isConnected()) + { + return; + } + synchronized (this.irc) + { + 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(final String memberId, final ChatRoomIrcImpl chatroom) + { + if (!isConnected()) + { + throw new IllegalStateException("Not connected to an IRC server."); + } + synchronized (this.irc) + { + this.irc.rawMessage("INVITE " + memberId + " " + + chatroom.getIdentifier()); + } + } + + /** + * Send a command to the IRC server. + * + * @param chatroom the chat room + * @param message the command message + */ + public void command(final ChatRoomIrcImpl chatroom, final String message) + { + this.command(chatroom.getIdentifier(), message); + } + + /** + * Send a command to the IRC server. + * + * @param contact the chat room + * @param message the command message + */ + public void command(final Contact contact, final MessageIrcImpl message) + { + this.command(contact.getAddress(), message.getContent()); + } + + /** + * Implementation of some commands. If the command is not recognized or + * implemented, it will be sent as if it were a normal message. + * + * TODO Eventually replace this with a factory such that we can easily + * extend with new commands. + * + * @param source Source contact or chat room from which the message is sent. + * @param message Command message that is sent. + */ + private void command(final String source, final String message) + { + if (!isConnected()) + { + throw new IllegalStateException("Not connected to IRC server."); + } + final String msg = message.toLowerCase(); + if (msg.startsWith("/msg ")) + { + final String part = message.substring(5); + int endOfNick = part.indexOf(' '); + if (endOfNick == -1) + { + throw new IllegalArgumentException("Invalid private message " + + "format. Message was not sent."); + } + final String target = part.substring(0, endOfNick); + final String command = part.substring(endOfNick + 1); + synchronized (this.irc) + { + this.irc.message(target, command); + } + } + else if (msg.startsWith("/me ")) + { + final String command = message.substring(4); + synchronized (this.irc) + { + this.irc.act(source, command); + } + } + else if (msg.startsWith("/join ")) + { + final String part = message.substring(6); + final String channel; + final String password; + int indexOfSep = part.indexOf(' '); + if (indexOfSep == -1) + { + channel = part; + password = ""; + } + else + { + channel = part.substring(0, indexOfSep); + password = part.substring(indexOfSep + 1); + } + if (channel.matches("[^,\\n\\r\\s\\a]+")) + { + synchronized (this.irc) + { + this.irc.joinChannel(channel, password); + } + } + } + else + { + synchronized (this.irc) + { + this.irc.message(source, message); + } + } + } + + /** + * Send an IRC message. + * + * @param chatroom The chat room to send the message to. + * @param message The message to send. + */ + public void message(final ChatRoomIrcImpl chatroom, final String message) + { + if (!isConnected()) + { + throw new IllegalStateException("Not connected to an IRC server."); + } + final String target = chatroom.getIdentifier(); + synchronized (this.irc) + { + this.irc.message(target, message); + } + } + + /** + * Send an IRC message. + * + * @param contact The contact to send the message to. + * @param message The message to send. + */ + public void message(final Contact contact, final Message message) + { + if (!isConnected()) + { + throw new IllegalStateException("Not connected to an IRC server."); + } + final String target = contact.getAddress(); + try + { + synchronized (this.irc) + { + this.irc.message(target, message.getContent()); + } + LOGGER.trace("Message delivered to server successfully."); + } + catch (RuntimeException e) + { + LOGGER.trace("Failed to deliver message: " + e.getMessage(), e); + throw e; + } + } + + /** + * Grant user permissions to specified user. + * + * @param chatRoom chat room to grant permissions for + * @param userAddress user to grant permissions to + * @param mode mode to grant + */ + public void grant(final ChatRoomIrcImpl chatRoom, final String userAddress, + final Mode mode) + { + if (!isConnected()) + { + throw new IllegalStateException("Not connected to an IRC server."); + } + if (mode.getRole() == null) + { + throw new IllegalArgumentException( + "This mode does not modify user permissions."); + } + synchronized (this.irc) + { + this.irc.changeMode(chatRoom.getIdentifier() + " +" + + mode.getSymbol() + " " + userAddress); + } + } + + /** + * Revoke user permissions of chat room for user. + * + * @param chatRoom chat room + * @param userAddress user + * @param mode mode + */ + public void revoke(final ChatRoomIrcImpl chatRoom, + final String userAddress, final Mode mode) + { + if (!isConnected()) + { + throw new IllegalStateException("Not connected to an IRC server."); + } + if (mode.getRole() == null) + { + throw new IllegalArgumentException( + "This mode does not modify user permissions."); + } + synchronized (this.irc) + { + this.irc.changeMode(chatRoom.getIdentifier() + " -" + + mode.getSymbol() + " " + userAddress); + } + } + + /** + * Prepare a chat room for initial opening. + * + * @param channel The IRC channel which is the source of data. + * @param chatRoom The chatroom to prepare. + */ + private void prepareChatRoom(final ChatRoomIrcImpl chatRoom, + final IRCChannel channel) + { + final IRCTopic topic = channel.getTopic(); + chatRoom.updateSubject(topic.getValue()); + + for (IRCUser user : channel.getUsers()) + { + ChatRoomMemberIrcImpl member = + new ChatRoomMemberIrcImpl(this.provider, chatRoom, + user.getNick(), ChatRoomMemberRole.SILENT_MEMBER); + ChatRoomMemberRole role; + for (IRCUserStatus status : channel.getStatusesForUser(user)) + { + role = convertMemberMode(status.getChanModeType().charValue()); + member.addRole(role); + } + chatRoom.addChatRoomMember(member.getContactAddress(), member); + if (this.getNick().equals(user.getNick())) + { + chatRoom.setLocalUser(member); + if (member.getRole() != ChatRoomMemberRole.SILENT_MEMBER) + { + ChatRoomLocalUserRoleChangeEvent event = + new ChatRoomLocalUserRoleChangeEvent(chatRoom, + ChatRoomMemberRole.SILENT_MEMBER, member.getRole(), + true); + chatRoom.fireLocalUserRoleChangedEvent(event); + } + } + } + } + + /** + * 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(final char modeSymbol) + { + return Mode.bySymbol(modeSymbol).getRole(); + } + + /** + * Calculate maximum message size that can be transmitted. + * + * @param contact receiving contact + * @return returns maximum message size + */ + public int calculateMaximumMessageSize(final Contact contact) + { + final StringBuilder builder = new StringBuilder(":"); + builder.append(getIdentityString()); + builder.append(" PRIVMSG "); + builder.append(contact.getAddress()); + builder.append(" :"); + return IRC_PROTOCOL_MAXIMUM_MESSAGE_SIZE - builder.length(); + } + + /** + * 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 final class ServerListener + extends VariousMessageListenerAdapter + { + /** + * IRC reply containing away message. + */ + private static final int RPL_AWAY = 301; + + /** + * IRC reply for WHOIS query. + */ + private static final int RPL_WHOISUSER = 311; + + /** + * IRC reply code for end of list. + */ + private static final int RPL_LISTEND = + IRCServerNumerics.CHANNEL_NICKS_END_OF_LIST; + + /** + * IRC error code for case of non-existing nick or channel name. + */ + private static final int ERR_NO_SUCH_NICK_CHANNEL = + IRCServerNumerics.NO_SUCH_NICK_CHANNEL; + + /** + * IRCApi instance. + */ + private final IRCApi irc; + + /** + * Constructor for Server Listener. + * + * @param irc IRCApi instance + */ + private ServerListener(final IRCApi irc) + { + if (irc == null) + { + throw new IllegalArgumentException( + "irc instance cannot be null"); + } + this.irc = irc; + } + + /** + * Print out server notices for debugging purposes and for simply + * keeping track of the connections. + * + * @param msg the server notice + */ + @Override + public void onServerNotice(final ServerNotice msg) + { + LOGGER.debug("NOTICE: " + 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(final ServerNumericMessage msg) + { + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("NUM MSG: " + msg.getNumericCode() + ": " + + msg.getText()); + } + + final Integer code = msg.getNumericCode(); + if (code == null) + { + LOGGER.debug("No 'code' in numeric message event."); + return; + } + + if (!IrcConnection.this.isConnected()) + { + // Skip message handling until we're officially connected. + return; + } + + switch (code.intValue()) + { + case RPL_LISTEND: + // CHANNEL_NICKS_END_OF_LIST indicates the end of a nick list as + // you will receive when joining a channel. This is used as the + // indicator that we have joined a channel. Now we have to + // determine whether or not we already know about this + // particular join attempt. If not, we continue to inform Jitsi + // and to create a listener for this new chat room. + final String text = msg.getText(); + final String channelName = text.substring(0, text.indexOf(' ')); + final ChatRoomIrcImpl chatRoom; + final IRCChannel channel; + synchronized (IrcConnection.this.joined) + { + // Synchronize the section that checks then adds a chat + // room. This way we can be sure that there are no 2 + // simultaneous creation events. + if (IrcConnection.this.joined.containsKey(channelName)) + { + LOGGER.trace("Chat room '" + channelName + + "' join event was announced or already " + + "finished. Stop handling this event."); + break; + } + // We aren't currently attempting to join, so this join is + // unannounced. + LOGGER.trace("Starting unannounced join of chat room '" + + channelName); + // Assuming that at the time that NICKS_END_OF_LIST is + // propagated, the channel join event has been completely + // handled by IRCApi. + channel = + IrcConnection.this.connectionState + .getChannelByName(channelName); + chatRoom = new ChatRoomIrcImpl( + channelName, IrcConnection.this.provider); + IrcConnection.this.joined.put(channelName, chatRoom); + } + this.irc.addListener(new ChatRoomListener(this.irc, chatRoom)); + try + { + IrcConnection.this.provider.getMUC().openChatRoomWindow( + chatRoom); + } + catch (NullPointerException e) + { + LOGGER.error("failed to open chat room window", e); + } + IrcConnection.this.prepareChatRoom(chatRoom, channel); + IrcConnection.this.provider.getMUC().fireLocalUserPresenceEvent( + chatRoom, + LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED, + null); + LOGGER.trace("Unannounced join of chat room '" + channelName + + "' completed."); + break; + + case ERR_NO_SUCH_NICK_CHANNEL: + // TODO Check if target is Contact, then update contact presence + // status to off-line since the nick apparently does not exist + // anymore. + if (LOGGER.isTraceEnabled()) + { + LOGGER.trace("Message did not get delivered: " + + msg.asRaw()); + } + final String msgText = msg.getText(); + final int endOfTargetIndex = msgText.indexOf(' '); + if (endOfTargetIndex == -1) + { + LOGGER.trace("Expected target nick in error message, but " + + "it cannot be found. Stop parsing."); + break; + } + final String targetNick = + msgText.substring(0, endOfTargetIndex); + // Send blank text string as the message, since we don't know + // what the actual message was. (We cannot reliably relate the + // NOSUCHNICK reply to the exact message that caused the error.) + MessageIrcImpl message = + new MessageIrcImpl( + "", + OperationSetBasicInstantMessaging.HTML_MIME_TYPE, + OperationSetBasicInstantMessaging.DEFAULT_MIME_ENCODING, + null); + final Contact to = + IrcConnection.this.provider.getPersistentPresence() + .findOrCreateContactByID(targetNick); + IrcConnection.this.provider + .getBasicInstantMessaging() + .fireMessageDeliveryFailed( + message, + to, + MessageDeliveryFailedEvent + .OFFLINE_MESSAGES_NOT_SUPPORTED); + break; + + case RPL_AWAY: + final String rawAwayText = msg.getText(); + final String awayUserNick = + rawAwayText.substring(0, rawAwayText.indexOf(' ')); + final String awayText = + rawAwayText.substring(rawAwayText.indexOf(' ') + 2); + final MessageIrcImpl awayMessage = + MessageIrcImpl.newAwayMessageFromIRC(awayText); + final Contact awayUser = + IrcConnection.this.provider.getPersistentPresence() + .findOrCreateContactByID(awayUserNick); + IrcConnection.this.provider.getBasicInstantMessaging() + .fireMessageReceived(awayMessage, awayUser); + break; + + case RPL_WHOISUSER: + final String whoismsg = msg.getText(); + final int endNickIndex = whoismsg.indexOf(' '); + final String nick = whoismsg.substring(0, endNickIndex); + if (!IrcConnection.this.connectionState.getNickname().equals(nick)) + { + // We need WHOIS info on ourselves to discover our identity + // on the IRC server. So skip other WHOIS replies. + return; + } + final int endUserIndex = + whoismsg.indexOf(' ', endNickIndex + 1); + final int endHostIndex = + whoismsg.indexOf(' ', endUserIndex + 1); + final String user = + whoismsg.substring(endNickIndex + 1, endUserIndex); + final String host = + whoismsg.substring(endUserIndex + 1, endHostIndex); + LOGGER.debug(String.format("Current identity: %s!%s@%s", + IrcConnection.this.connectionState.getNickname(), user, + host)); + IrcConnection.this.identity = + new IrcConnection.Identity(user, host); + break; + + default: + if (LOGGER.isTraceEnabled()) + { + LOGGER.trace("This ServerNumericMessage (" + code + + ") will not be handled by the ServerListener."); + } + break; + } + } + + /** + * 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(final ErrorMessage msg) + { + if (LOGGER.isDebugEnabled()) + { + LOGGER + .debug("ERROR: " + msg.getSource() + ": " + msg.getText()); + } + if (IrcConnection.this.connectionState != null) + { + if (!IrcConnection.this.connectionState.isConnected()) + { + IrcConnection.this.provider + .setCurrentRegistrationState( + RegistrationState.CONNECTION_FAILED); + } + } + } + + /** + * Upon receiving a private message from a user, deliver that to an + * instant messaging contact 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(final UserPrivMsg msg) + { + final String user = msg.getSource().getNick(); + final MessageIrcImpl message = + MessageIrcImpl.newMessageFromIRC(msg.getText()); + final Contact from = + IrcConnection.this.provider.getPersistentPresence() + .findOrCreateContactByID(user); + try + { + IrcConnection.this.provider.getBasicInstantMessaging() + .fireMessageReceived(message, from); + } + catch (RuntimeException e) + { + // TODO remove once this is stable. Don't want to lose message + // when an accidental error occurs. + // It is likely that errors occurred because of some issues with + // MetaContactGroup for NonPersistent group, since this is an + // outstanding error. + LOGGER.error( + "Error occurred while delivering private message from user" + + " '" + user + "': " + msg.getText(), e); + } + } + + /** + * Upon receiving a user notice message from a user, deliver that to an + * instant messaging contact. + * + * @param msg user notice message + */ + @Override + public void onUserNotice(final UserNotice msg) + { + final String user = msg.getSource().getNick(); + final Contact from = + IrcConnection.this.provider.getPersistentPresence() + .findOrCreateContactByID(user); + final MessageIrcImpl message = + MessageIrcImpl.newNoticeFromIRC(from, msg.getText()); + IrcConnection.this.provider.getBasicInstantMessaging() + .fireMessageReceived(message, from); + } + + /** + * Upon receiving a user action message from a user, deliver that to an + * instant messaging contact. + * + * @param msg user action message + */ + @Override + public void onUserAction(final UserActionMsg msg) + { + final String user = msg.getSource().getNick(); + final Contact from = + IrcConnection.this.provider.getPersistentPresence() + .findContactByID(user); + final MessageIrcImpl message = + MessageIrcImpl.newActionFromIRC(from, msg.getText()); + IrcConnection.this.provider.getBasicInstantMessaging() + .fireMessageReceived(message, from); + } + + /** + * User quit messages. + * + * User quit messages only need to be handled in case quitting users, + * since that is the only clear signal of presence change we have. + * + * @param msg Quit message + */ + @Override + public void onUserQuit(final QuitMessage msg) + { + final String user = msg.getSource().getNick(); + if (user != null + && user + .equals(IrcConnection.this.connectionState.getNickname())) + { + LOGGER.debug("Local user's QUIT message received: removing " + + "server listener."); + this.irc.deleteListener(this); + return; + } + } + } + + /** + * A chat room listener. + * + * A chat room listener is registered for each chat room that we join. The + * chat room listener updates chat room data and fires events based on IRC + * messages that report state changes for the specified channel. + * + * @author Danny van Heumen + * + */ + private final class ChatRoomListener + extends VariousMessageListenerAdapter + { + /** + * IRC error code for case when user cannot send a message to the + * channel, for example when this channel is moderated and user does not + * have VOICE (+v). + */ + private static final int IRC_ERR_CANNOTSENDTOCHAN = 404; + + /** + * IRC error code for case where user is not joined to that channel. + */ + private static final int IRC_ERR_NOTONCHANNEL = 442; + + /** + * IRCApi instance. + */ + private final IRCApi irc; + + /** + * Chat room for which this listener is working. + */ + private final ChatRoomIrcImpl chatroom; + + /** + * Constructor. Instantiate listener for the provided chat room. + * + * @param irc IRCApi instance + * @param chatroom the chat room + */ + private ChatRoomListener(final IRCApi irc, + final ChatRoomIrcImpl chatroom) + { + if (chatroom == null) + { + throw new IllegalArgumentException("chatroom cannot be null"); + } + this.chatroom = chatroom; + if (irc == null) + { + throw new IllegalArgumentException("irc cannot be null"); + } + this.irc = irc; + } + + /** + * Event in case of topic change. + * + * @param msg topic change message + */ + @Override + public void onTopicChange(final TopicMessage msg) + { + if (!isThisChatRoom(msg.getChannelName())) + { + return; + } + + // FIXME Topic change event report message interprets HTML chars in + // channel name. + this.chatroom.updateSubject(msg.getTopic().getValue()); + } + + /** + * Event in case of channel mode changes. + * + * @param msg channel mode message + */ + @Override + public void onChannelMode(final ChannelModeMessage msg) + { + if (!isThisChatRoom(msg.getChannelName())) + { + return; + } + + processModeMessage(msg); + } + + /** + * Event in case of channel join message. + * + * @param msg channel join message + */ + @Override + public void onChannelJoin(final ChanJoinMessage msg) + { + if (!isThisChatRoom(msg.getChannelName())) + { + return; + } + + final String user = msg.getSource().getNick(); + final ChatRoomMemberIrcImpl member = + new ChatRoomMemberIrcImpl(IrcConnection.this.provider, + this.chatroom, user, ChatRoomMemberRole.SILENT_MEMBER); + this.chatroom.fireMemberPresenceEvent(member, null, + ChatRoomMemberPresenceChangeEvent.MEMBER_JOINED, null); + } + + /** + * Event in case of channel part. + * + * @param msg channel part message + */ + @Override + public void onChannelPart(final ChanPartMessage msg) + { + if (!isThisChatRoom(msg.getChannelName())) + { + return; + } + + final IRCUser user = msg.getSource(); + if (isMe(user)) + { + leaveChatRoom(); + return; + } + + final String userNick = msg.getSource().getNick(); + final ChatRoomMember member = + this.chatroom.getChatRoomMember(userNick); + if (member != null) + { + // When the account has been disabled, the chat room may return + // null. If that is NOT the case, continue handling. + try + { + this.chatroom.fireMemberPresenceEvent(member, null, + ChatRoomMemberPresenceChangeEvent.MEMBER_LEFT, + msg.getPartMsg()); + } + catch (NullPointerException e) + { + LOGGER.warn( + "This should not have happened. Please report this " + + "as it is a bug.", e); + } + } + } + + /** + * Some of the generic message are relevant to us, so keep an eye on + * general numeric messages. + * + * @param msg IRC server numeric message + */ + public void onServerNumericMessage(final ServerNumericMessage msg) + { + final Integer code = msg.getNumericCode(); + if (code == null) + { + return; + } + final String raw = msg.getText(); + switch (code.intValue()) + { + case IRC_ERR_NOTONCHANNEL: + final String channel = raw.substring(0, raw.indexOf(" ")); + if (isThisChatRoom(channel)) + { + LOGGER + .warn("Just discovered that we are no longer joined to " + + "channel " + + channel + + ". Leaving quietly. (This is most likely due to a" + + " bug in the implementation.)"); + // If for some reason we missed the message that we aren't + // joined (anymore) to this particular chat room, correct + // our problem ASAP. + leaveChatRoom(); + } + break; + + case IRC_ERR_CANNOTSENDTOCHAN: + final String cannotSendChannel = + raw.substring(0, raw.indexOf(" ")); + if (isThisChatRoom(cannotSendChannel)) + { + final MessageIrcImpl message = + new MessageIrcImpl("", "text/plain", "UTF-8", null); + this.chatroom.fireMessageDeliveryFailedEvent( + ChatRoomMessageDeliveryFailedEvent.FORBIDDEN, + "This channel is moderated.", new Date(), message); + } + break; + + default: + break; + } + } + + /** + * Event in case of channel kick. + * + * @param msg channel kick message + */ + @Override + public void onChannelKick(final ChannelKick msg) + { + if (!isThisChatRoom(msg.getChannelName())) + { + return; + } + + if (!IrcConnection.this.isConnected()) + { + LOGGER.error("Not currently connected to IRC Server. " + + "Aborting message handling."); + return; + } + + final String kickedUser = msg.getKickedNickname(); + final ChatRoomMember kickedMember = + this.chatroom.getChatRoomMember(kickedUser); + final String user = msg.getSource().getNick(); + if (kickedMember != null) + { + ChatRoomMember kicker = this.chatroom.getChatRoomMember(user); + this.chatroom.fireMemberPresenceEvent(kickedMember, kicker, + ChatRoomMemberPresenceChangeEvent.MEMBER_KICKED, + msg.getText()); + } + if (isMe(kickedUser)) + { + LOGGER.debug( + "Local user is kicked. Removing chat room listener."); + this.irc.deleteListener(this); + IrcConnection.this.joined.remove(this.chatroom.getIdentifier()); + IrcConnection.this.provider.getMUC().fireLocalUserPresenceEvent( + this.chatroom, + LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_KICKED, + msg.getText()); + } + } + + /** + * Event in case of user quit. + * + * @param msg user quit message + */ + @Override + public void onUserQuit(final QuitMessage msg) + { + String user = msg.getSource().getNick(); + if (user == null) + { + return; + } + if (user.equals(IrcConnection.this.connectionState.getNickname())) + { + LOGGER.debug("Local user QUIT message received: removing chat " + + "room listener."); + this.irc.deleteListener(this); + return; + } + final ChatRoomMember member = this.chatroom.getChatRoomMember(user); + if (member != null) + { + this.chatroom.fireMemberPresenceEvent(member, null, + ChatRoomMemberPresenceChangeEvent.MEMBER_QUIT, + msg.getQuitMsg()); + } + } + + /** + * Event in case of nick change. + * + * @param msg nick change message + */ + @Override + public void onNickChange(final NickMessage msg) + { + if (msg == null) + { + return; + } + + final String oldNick = msg.getSource().getNick(); + final String newNick = msg.getNewNick(); + + final ChatRoomMemberIrcImpl member = + (ChatRoomMemberIrcImpl) this.chatroom + .getChatRoomMember(oldNick); + if (member != null) + { + member.setName(newNick); + this.chatroom.updateChatRoomMemberName(oldNick); + ChatRoomMemberPropertyChangeEvent evt = + new ChatRoomMemberPropertyChangeEvent(member, + this.chatroom, + ChatRoomMemberPropertyChangeEvent.MEMBER_NICKNAME, + oldNick, newNick); + this.chatroom.fireMemberPropertyChangeEvent(evt); + } + } + + /** + * Event in case of channel message arrival. + * + * @param msg channel message + */ + @Override + public void onChannelMessage(final ChannelPrivMsg msg) + { + if (!isThisChatRoom(msg.getChannelName())) + { + return; + } + + final MessageIrcImpl message = + MessageIrcImpl.newMessageFromIRC(msg.getText()); + final ChatRoomMemberIrcImpl member = + new ChatRoomMemberIrcImpl(IrcConnection.this.provider, + this.chatroom, msg.getSource().getNick(), + ChatRoomMemberRole.MEMBER); + this.chatroom.fireMessageReceivedEvent(message, member, new Date(), + ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED); + } + + /** + * Event in case of channel action message arrival. + * + * @param msg channel action message + */ + @Override + public void onChannelAction(final ChannelActionMsg msg) + { + if (!isThisChatRoom(msg.getChannelName())) + { + return; + } + + String userNick = msg.getSource().getNick(); + ChatRoomMemberIrcImpl member = + new ChatRoomMemberIrcImpl(IrcConnection.this.provider, + this.chatroom, userNick, ChatRoomMemberRole.MEMBER); + MessageIrcImpl message = + MessageIrcImpl.newActionFromIRC(member, msg.getText()); + this.chatroom.fireMessageReceivedEvent(message, member, new Date(), + ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED); + } + + /** + * Event in case of channel notice message arrival. + * + * @param msg channel notice message + */ + @Override + public void onChannelNotice(final ChannelNotice msg) + { + if (!isThisChatRoom(msg.getChannelName())) + { + return; + } + + final String userNick = msg.getSource().getNick(); + final ChatRoomMemberIrcImpl member = + new ChatRoomMemberIrcImpl(IrcConnection.this.provider, + this.chatroom, userNick, ChatRoomMemberRole.MEMBER); + final MessageIrcImpl message = + MessageIrcImpl.newNoticeFromIRC(member, msg.getText()); + this.chatroom.fireMessageReceivedEvent(message, member, new Date(), + ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED); + } + + /** + * Leave this chat room. + */ + private void leaveChatRoom() + { + this.irc.deleteListener(this); + IrcConnection.this.joined.remove(this.chatroom.getIdentifier()); + LOGGER.debug("Leaving chat room " + this.chatroom.getIdentifier() + + ". Chat room listener removed."); + IrcConnection.this.provider.getMUC().fireLocalUserPresenceEvent( + this.chatroom, + LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_LEFT, null); + } + + /** + * Process mode changes. + * + * @param msg raw mode message + */ + private void processModeMessage(final ChannelModeMessage msg) + { + final ChatRoomMemberIrcImpl source = extractChatRoomMember(msg); + final ModeParser parser = new ModeParser(msg.getModeStr()); + for (ModeEntry mode : parser.getModes()) + { + switch (mode.getMode()) + { + case OWNER: + case OPERATOR: + case HALFOP: + case VOICE: + processRoleChange(source, mode); + break; + case LIMIT: + processLimitChange(source, mode); + break; + case BAN: + processBanChange(source, mode); + break; + case UNKNOWN: + if (LOGGER.isInfoEnabled()) + { + LOGGER.info("Unknown mode: " + + (mode.isAdded() ? "+" : "-") + + mode.getParams()[0] + ". Original mode string: '" + + msg.getModeStr() + "'"); + } + break; + default: + if (LOGGER.isInfoEnabled()) + { + LOGGER.info("Unsupported mode '" + + (mode.isAdded() ? "+" : "-") + mode.getMode() + + "' (from modestring '" + msg.getModeStr() + "')"); + } + break; + } + } + } + + /** + * Process changes for ban patterns. + * + * @param sourceMember the originating member + * @param mode the ban mode change + */ + private void processBanChange(final ChatRoomMemberIrcImpl sourceMember, + final ModeEntry mode) + { + final MessageIrcImpl banMessage = + new MessageIrcImpl( + "channel ban mask was " + + (mode.isAdded() ? "added" : "removed") + + ": " + + mode.getParams()[0] + + " by " + + (sourceMember.getContactAddress().length() == 0 + ? "server" + : sourceMember.getContactAddress()), + MessageIrcImpl.DEFAULT_MIME_TYPE, + MessageIrcImpl.DEFAULT_MIME_ENCODING, null); + this.chatroom.fireMessageReceivedEvent(banMessage, sourceMember, + new Date(), + ChatRoomMessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED); + } + + /** + * Process mode changes resulting in role manipulation. + * + * @param sourceMember the originating member + * @param mode the mode change + */ + private void processRoleChange( + final ChatRoomMemberIrcImpl sourceMember, final ModeEntry mode) + { + final String targetNick = mode.getParams()[0]; + final ChatRoomMemberIrcImpl targetMember = + (ChatRoomMemberIrcImpl) this.chatroom + .getChatRoomMember(targetNick); + final ChatRoomMemberRole originalRole = targetMember.getRole(); + if (mode.isAdded()) + { + targetMember.addRole(mode.getMode().getRole()); + } + else + { + targetMember.removeRole(mode.getMode().getRole()); + } + final ChatRoomMemberRole newRole = targetMember.getRole(); + if (newRole != originalRole) + { + // Mode change actually caused a role change. + final ChatRoomLocalUserRoleChangeEvent event = + new ChatRoomLocalUserRoleChangeEvent(this.chatroom, + originalRole, newRole, false); + if (isMe(targetMember.getContactAddress())) + { + this.chatroom.fireLocalUserRoleChangedEvent(event); + } + else + { + this.chatroom.fireMemberRoleEvent(targetMember, + newRole); + } + } + else + { + // Mode change did not cause an immediate role change. + // Display a system message for the mode change. + final String text = + sourceMember.getName() + + (mode.isAdded() ? " gives " + + mode.getMode().name().toLowerCase() + + " to " : " removes " + + mode.getMode().name().toLowerCase() + + " from ") + targetMember.getName(); + final MessageIrcImpl message = + new MessageIrcImpl(text, + MessageIrcImpl.DEFAULT_MIME_TYPE, + MessageIrcImpl.DEFAULT_MIME_ENCODING, null); + this.chatroom + .fireMessageReceivedEvent( + message, + sourceMember, + new Date(), + ChatRoomMessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED); + } + } + + /** + * Process mode change that represents a channel limit modification. + * + * @param sourceMember the originating member + * @param mode the limit mode change + */ + private void processLimitChange( + final ChatRoomMemberIrcImpl sourceMember, final ModeEntry mode) + { + final MessageIrcImpl limitMessage; + if (mode.isAdded()) + { + try + { + limitMessage = + new MessageIrcImpl( + "channel limit set to " + + Integer.parseInt(mode.getParams()[0]) + + " by " + + (sourceMember.getContactAddress() + .length() == 0 + ? "server" + : sourceMember.getContactAddress()), + "text/plain", "UTF-8", null); + } + catch (NumberFormatException e) + { + LOGGER.warn("server sent incorrect limit: " + + "limit is not a number", e); + return; + } + } + else + { + // TODO "server" is now easily fakeable if someone + // calls himself server. There should be some other way + // to represent the server if a message comes from + // something other than a normal chat room member. + limitMessage = + new MessageIrcImpl( + "channel limit removed by " + + (sourceMember.getContactAddress().length() == 0 + ? "server" + : sourceMember.getContactAddress()), + "text/plain", "UTF-8", null); + } + this.chatroom.fireMessageReceivedEvent(limitMessage, sourceMember, + new Date(), + ChatRoomMessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED); + } + + /** + * Extract chat room member identifier from message. + * + * @param msg raw mode message + * @return returns member instance + */ + private ChatRoomMemberIrcImpl extractChatRoomMember( + final ChannelModeMessage msg) + { + ChatRoomMemberIrcImpl member; + ISource source = msg.getSource(); + if (source instanceof IRCServer) + { + // TODO Created chat room member with creepy empty contact ID. + // Interacting with this contact might screw up other sections + // of code which is not good. Is there a better way to represent + // an IRC server as a chat room member? + member = + new ChatRoomMemberIrcImpl(IrcConnection.this.provider, + this.chatroom, "", ChatRoomMemberRole.ADMINISTRATOR); + } + else if (source instanceof IRCUser) + { + String nick = ((IRCUser) source).getNick(); + member = + (ChatRoomMemberIrcImpl) this.chatroom + .getChatRoomMember(nick); + } + else + { + throw new IllegalArgumentException("Unknown source type: " + + source.getClass().getName()); + } + return member; + } + + /** + * Test whether this listener corresponds to the chat room. + * + * @param chatRoomName chat room name + * @return returns true if this listener applies, false otherwise + */ + private boolean isThisChatRoom(final String chatRoomName) + { + return this.chatroom.getIdentifier().equalsIgnoreCase(chatRoomName); + } + + /** + * Test whether the source user is this user. + * + * @param user the source user + * @return returns true if this use, or false otherwise + */ + private boolean isMe(final IRCUser user) + { + return isMe(user.getNick()); + } + + /** + * Test whether the user nick is this user. + * + * @param name nick of the user + * @return returns true if so, false otherwise + */ + private boolean isMe(final String name) + { + final String userNick = + IrcConnection.this.connectionState.getNickname(); + if (userNick == null) + { + return false; + } + return userNick.equals(name); + } + } + + /** + * Special listener that processes LIST replies and signals once the list is + * completely filled. + */ + private static final class ChannelListListener + extends VariousMessageListenerAdapter + { + /** + * Start of an IRC server channel listing reply. + */ + private static final int RPL_LISTSTART = 321; + + /** + * Continuation of an IRC server channel listing reply. + */ + private static final int RPL_LIST = 322; + + /** + * End of an IRC server channel listing reply. + */ + private static final int RPL_LISTEND = 323; + + /** + * Reference to the IRC API instance. + */ + private final IRCApi api; + + /** + * Reference to the provided list instance. + */ + private final Result<List<String>, Exception> signal; + + /** + * Constructor for channel list listener. + * + * @param api irc-api library instance + * @param signal signal for sync signaling + */ + private ChannelListListener(final IRCApi api, + final Result<List<String>, Exception> signal) + { + if (api == null) + { + throw new IllegalArgumentException( + "IRC api instance cannot be null"); + } + this.api = api; + this.signal = signal; + } + + /** + * Act on LIST messages: 321 RPL_LISTSTART, 322 RPL_LIST, 323 + * RPL_LISTEND + * + * Clears the list upon starting. All received channels are added to the + * list. Upon receiving RPL_LISTEND finalize the list and signal the + * waiting thread that it can continue processing the list. + * + * @param msg The numeric server message. + */ + @Override + public void onServerNumericMessage(final ServerNumericMessage msg) + { + if (this.signal.isDone()) + { + return; + } + + switch (msg.getNumericCode()) + { + case RPL_LISTSTART: + synchronized (this.signal) + { + this.signal.getValue().clear(); + } + break; + case RPL_LIST: + String channel = parse(msg.getText()); + if (channel != null) + { + synchronized (this.signal) + { + this.signal.getValue().add(channel); + } + } + break; + case RPL_LISTEND: + synchronized (this.signal) + { + // Done collecting channels. Remove listener and then we're + // done. + this.api.deleteListener(this); + this.signal.setDone(); + this.signal.notifyAll(); + } + break; + // TODO Add support for REPLY 416: LIST :output too large, truncated + default: + break; + } + } + + /** + * Parse an IRC server response RPL_LIST. Extract the channel name. + * + * @param text raw server response + * @return returns the channel name + */ + private String parse(final String text) + { + int endOfChannelName = text.indexOf(' '); + if (endOfChannelName == -1) + { + return null; + } + // Create a new string to make sure that the original (larger) + // strings can be GC'ed. + return new String(text.substring(0, endOfChannelName)); + } + } + + /** + * Task for cleaning up old channel list caches. + * + * @author Danny van Heumen + */ + private static final class ChannelListCacheCleanUpTask + extends TimerTask + { + /** + * Expected timestamp on which the list cache was created. It is used as + * an indicator to see whether the cache has been refreshed in the mean + * time. + */ + private final long timestamp; + + /** + * Container holding the channel list cache. + */ + private final Container<List<String>> container; + + /** + * Construct new clean up job definition. + * + * @param listContainer container that holds the channel list cache + * @param timestamp expected timestamp of list cache creation + */ + private ChannelListCacheCleanUpTask( + final Container<List<String>> listContainer, final long timestamp) + { + if (listContainer == null) + { + throw new IllegalArgumentException( + "listContainer cannot be null"); + } + this.container = listContainer; + this.timestamp = timestamp; + } + + /** + * Remove the list reference from the container. But only if the + * timestamp matches. This makes sure that only one clean up job will + * clean up a list. + */ + @Override + public void run() + { + synchronized (this.container) + { + // Only clean up old cache if this is the dedicated task for it. + // If the timestamp has changed, another job is responsible for + // the clean up. + if (this.container.getTimestamp() != this.timestamp) + { + LOGGER.trace("Not cleaning up channel list cache. The " + + "timestamp does not match."); + return; + } + this.container.set(null); + } + // We cannot clear the list itself, since the contents might still + // be in use by the UI, inside the immutable wrapper. + LOGGER.debug("Old channel list cache has been cleared."); + } + } + + /** + * Simplest possible container that we can use for locking while we're + * checking/modifying the contents. + * + * @param <T> The type of instance to store in the container + */ + private static final class Container<T> + { + /** + * The stored instance. (Can be null) + */ + private T instance; + + /** + * Time of stored instance. + */ + private long time; + + /** + * Constructor that immediately sets the instance. + * + * @param instance the instance to set + */ + private Container(final T instance) + { + this.instance = instance; + this.time = System.nanoTime(); + } + + /** + * Conditionally get the stored instance. Get the instance when time + * difference is within specified bound. Otherwise return null. + * + * @param bound maximum time difference that is allowed. + * @return returns instance if within bounds, or null otherwise + */ + public T get(final long bound) + { + if (System.nanoTime() - this.time > bound) + { + return null; + } + return this.instance; + } + + /** + * Set an instance. + * + * @param instance the instance + */ + public void set(final T instance) + { + this.instance = instance; + this.time = System.nanoTime(); + } + + /** + * Get the timestamp from when the instance was set. + * + * @return returns the timestamp + */ + public long getTimestamp() + { + return this.time; + } + } + + /** + * Storage container for identity components. + * + * IRC identity components user and host are stored. The nick name component + * isn't stored, because it changes too frequently. When getting the + * identity string, the nick name component is provided at calling time. + * + * @author Danny van Heumen + */ + private static final class Identity + { + /** + * User name. + */ + private final String user; + + /** + * Host name. + */ + private final String host; + + /** + * Constructor. + * + * @param user user + * @param host host + */ + private Identity(final String user, final String host) + { + this.user = user; + this.host = host; + } + + /** + * Get identity string. + * + * @param currentNick the current nick + * @return returns identity string + */ + public String getIdentityString(final String currentNick) + { + return String.format("%s!%s@%s", currentNick, this.user, this.host); + } + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java b/src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java index 4845dd9..b741cf1 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java @@ -13,126 +13,40 @@ import java.util.concurrent.atomic.*; import javax.net.ssl.*; -import net.java.sip.communicator.impl.protocol.irc.ModeParser.ModeEntry; import net.java.sip.communicator.service.certificate.*; 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. * - * TODO Do we need to cancel any join channel operations still in progress? - * - * <p> - * Common IRC network facilities: - * </p> - * - * <ul> - * <li>NickServ - nick related services (also allow setting NickServ nick - - * there are cases where the name is different)</li> - * <li>ChanServ - channel related services</li> - * <li>MemoServ - message relaying services</li> - * </ul> - * * @author Danny van Heumen */ public class IrcStack { /** - * TODO In the far far future ... - * - * <p> - * Some of the less pressing features that may one day be useful ... - * </p> - * - * <pre> - * - Handle 404 ERR_CANNOTSENDTOCHAN in case of +n channel mode and not - * joined to the channel where you send a message to. - * </pre> - */ - - /** - * Maximum message size for IRC messages given the spec specifies a buffer - * of 512 bytes. The command ending (CRLF) takes up 2 bytes. - */ - private static final int IRC_PROTOCOL_MAXIMUM_MESSAGE_SIZE = 510; - - /** - * Clean-up delay. The clean up task clears any remaining chat room list - * cache. Since there's no pointing in timing it exactly, delay the clean up - * until after expiration. - */ - private static final long CACHE_CLEAN_UP_DELAY = 1000L; - - /** - * Ratio of milliseconds to nanoseconds for conversions. - */ - private static final long RATIO_MILLISECONDS_TO_NANOSECONDS = 1000000L; - - /** - * Expiration time for chat room list cache. - */ - private static final long CHAT_ROOM_LIST_CACHE_EXPIRATION = 60000000000L; - - /** * Logger. */ private static final Logger LOGGER = Logger.getLogger(IrcStack.class); /** - * Set of characters with special meanings for IRC, such as: ',' used as - * separator of list of items (channels, nicks, etc.), ' ' (space) separator - * of command parameters, etc. - */ - public static final Set<Character> SPECIAL_CHARACTERS; - - /** - * Initialize set of special characters. - */ - static { - HashSet<Character> specials = new HashSet<Character>(); - specials.add('\0'); - specials.add('\n'); - specials.add('\r'); - specials.add(' '); - specials.add(','); - SPECIAL_CHARACTERS = Collections.unmodifiableSet(specials); - } - - /** * Parent provider for IRC. */ private final ProtocolProviderServiceIrcImpl provider; /** - * Container for joined channels. - * - * There are two different cases: - * - * <pre> - * - null value: joining is initiated but still in progress. - * - non-null value: joining is finished, chat room instance is available. - * </pre> - */ - 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 contained in an AtomicReference. + * Instance of the irc connection contained in an AtomicReference. * * This field serves 2 purposes: * @@ -141,36 +55,11 @@ public class IrcStack * unlocking. By synchronizing we have connect and disconnect operations * wait for each other. * - * Second is to get the current api instance. AtomicReference ensures that - * we either get the old or the new instance. - */ - private final AtomicReference<IRCApi> session = - new AtomicReference<IRCApi>(null); - - /** - * Connection state of a successful IRC connection. + * Second is to get the current connection instance. AtomicReference ensures + * that we either get the old or the new instance. */ - private IIRCState connectionState; - - /** - * Manager component that manages current IRC presence. - */ - private PresenceManager presence = null; - - /** - * The local user's identity as it will be used in server-client - * communication for sent messages. - */ - private Identity identity; - - /** - * 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); + private final AtomicReference<IrcConnection> session = + new AtomicReference<IrcConnection>(null); /** * Constructor. @@ -194,27 +83,6 @@ public class IrcStack } /** - * Check whether or not a connection is established. - * - * @return true if connected, false otherwise. - */ - public boolean isConnected() - { - return (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 @@ -229,14 +97,6 @@ public class IrcStack final String password, final boolean secureConnection, final boolean autoNickChange) throws Exception { - if (isConnected()) - { - return; - } - - // Make sure we start with an empty joined-channel list. - this.joined.clear(); - final IRCServer server; if (secureConnection) { @@ -251,10 +111,14 @@ public class IrcStack synchronized (this.session) { + final IrcConnection current = this.session.get(); + if (current != null && current.isConnected()) + { + return; + } + final IRCApi irc = new IRCApiImpl(true); this.params.setServer(server); - this.session.set(irc); - irc.addListener(new ServerListener(irc)); if (LOGGER.isTraceEnabled()) { @@ -265,134 +129,42 @@ public class IrcStack try { - connectSynchronized(); - - // instantiate presence manager for the connection - this.presence = - new PresenceManager(irc, this.connectionState, - this.provider.getPersistentPresence()); - - queryIdentity(); + final IrcConnection connection = + new IrcConnection(this.provider, this.params, irc); + this.session.set(connection); // if connecting succeeded, set state to registered this.provider .setCurrentRegistrationState(RegistrationState.REGISTERED); - - // TODO Read IRC network capabilities based on RPL_ISUPPORT - // (005) - // replies if available. This information should be available in - // irc-api if possible. } catch (IOException e) { // Also SSL exceptions will be caught here. this.provider - .setCurrentRegistrationState( - RegistrationState.CONNECTION_FAILED); + .setCurrentRegistrationState(RegistrationState + .CONNECTION_FAILED); throw e; } catch (InterruptedException e) { this.provider - .setCurrentRegistrationState( - RegistrationState.UNREGISTERED); + .setCurrentRegistrationState(RegistrationState + .UNREGISTERED); throw e; } } } /** - * Perform synchronized connect operation. + * Get the current connection instance. * - * @throws Exception exception thrown when connect fails + * @return returns current connection instance or null if no connection is + * established. */ - private void connectSynchronized() throws Exception + public IrcConnection getConnection() { - final IRCApi irc = this.session.get(); - if (irc == null) - { - throw new IllegalStateException( - "No IRC instance available, cannot connect."); - } - final Result<IIRCState, Exception> result = - new Result<IIRCState, Exception>(); - synchronized (result) - { - // start connecting to the specified server ... - irc.connect(this.params, new Callback<IIRCState>() - { - - @Override - public void onSuccess(final IIRCState state) - { - synchronized (result) - { - LOGGER.trace("IRC connected successfully!"); - result.setDone(state); - result.notifyAll(); - } - } - - @Override - public void onFailure(final Exception e) - { - synchronized (result) - { - LOGGER.trace("IRC connection FAILED!", e); - result.setDone(e); - result.notifyAll(); - } - } - }); - - this.provider - .setCurrentRegistrationState(RegistrationState.REGISTERING); - - while (!result.isDone()) - { - LOGGER.trace("Waiting for the connection to be " - + "established ..."); - result.wait(); - } - - // TODO Implement connection timeout and a way to recognize that - // the timeout occurred. - - final Exception e = result.getException(); - if (e != null) - { - throw new IOException(e); - } - - final IIRCState connectionState = result.getValue(); - if (connectionState == null) { - throw new IOException("Failed to connect: no connection state available."); - } - - this.connectionState = connectionState; - } - } - - /** - * Issue WHOIS query to discover identity as seen by the server. - */ - private void queryIdentity() - { - // TODO Install temporary whois listener that handles the result. - this.session.get().rawMessage( - "WHOIS " + this.connectionState.getNickname()); - } - - /** - * Get the current identity string, based on nick, user and host of local - * user. - * - * @return returns identity string - */ - public String getIdentityString() - { - final String currentNick = this.connectionState.getNickname(); - return this.identity.getIdentityString(currentNick); + // FIXME do we need to check all users of getConnection() to see if they test for getConnection() == null? + return this.session.get(); } /** @@ -427,30 +199,16 @@ public class IrcStack */ public void disconnect() { + final IrcConnection connection; synchronized (this.session) { - final IRCApi irc = this.session.get(); - if (irc == null) - { - return; - } - try - { - irc.disconnect(); - } - catch (RuntimeException e) - { - // Disconnect might throw ChannelClosedException. Shouldn't be a - // problem, but for now lets log it just to be sure. - LOGGER.debug("exception occurred while disconnecting", e); - } - // Even though we set a new connectionState instance upon - // connecting, it is important that we null it here. In the case of - // an unannounced disconnect (for example system suspends) the - // connectionState will not be updated to reflect the disconnected - // state, i.e. it is out-of-sync with reality. - this.connectionState = null; - this.session.set(null); + // synchronization needed to ensure that no other process (such as + // connection attempt) is in progress + connection = this.session.getAndSet(null); + } + if (connection != null) + { + connection.disconnect(); } this.provider .setCurrentRegistrationState(RegistrationState.UNREGISTERED); @@ -465,1877 +223,6 @@ public class IrcStack } /** - * Get a set of channel type indicators. - * - * @return returns set of channel type indicators. - */ - public Set<Character> getChannelTypes() - { - if (!isConnected()) - { - throw new IllegalStateException("not connected to IRC server"); - } - return this.connectionState.getServerOptions().getChanTypes(); - } - - /** - * 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() - { - if (this.connectionState == null) - { - return this.params.getNickname(); - } - else - { - return this.connectionState.getNickname(); - } - } - - /** - * Set the user's new nick name. - * - * @param nick the new nick name - */ - public void setUserNickname(final String nick) - { - LOGGER.trace("Setting user's nick name to " + nick); - if (isConnected()) - { - final IRCApi irc = this.session.get(); - irc.changeNick(nick); - } - else - { - this.params.setNickname(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(final ChatRoomIrcImpl chatroom, final String subject) - { - if (!isConnected()) - { - throw new IllegalStateException( - "Please connect to an IRC server first."); - } - if (chatroom == null) - { - throw new IllegalArgumentException("Cannot have a null chatroom"); - } - LOGGER.trace("Setting chat room topic to '" + subject + "'"); - this.session.get().changeTopic(chatroom.getIdentifier(), - 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(final ChatRoomIrcImpl chatroom) - { - return this.joined.get(chatroom.getIdentifier()) != null; - } - - /** - * 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."); - if (!isConnected()) - { - throw new IllegalStateException("Not connected to an IRC server."); - } - - final IRCApi irc = this.session.get(); - - // TODO Currently, not using an API library method for listing - // channels, since it isn't available. - synchronized (this.channellist) - { - List<String> list = - this.channellist.get(CHAT_ROOM_LIST_CACHE_EXPIRATION); - if (list == null) - { - LOGGER - .trace("Chat room list null or outdated. Start retrieving " - + "new chat room list."); - Result<List<String>, Exception> listSignal = - new Result<List<String>, Exception>( - new LinkedList<String>()); - synchronized (listSignal) - { - try - { - irc.addListener( - new ChannelListListener(irc, listSignal)); - irc.rawMessage("LIST"); - while (!listSignal.isDone()) - { - LOGGER.trace("Waiting for list ..."); - listSignal.wait(); - } - LOGGER.trace("Done waiting for list."); - } - catch (InterruptedException e) - { - LOGGER.warn("INTERRUPTED while waiting for list.", e); - } - } - list = listSignal.getValue(); - this.channellist.set(list); - LOGGER.trace("Finished retrieving server chat room list."); - - // Set timer to clean up the cache after use, since otherwise it - // could stay in memory for a long time. - createCleanUpJob(this.channellist); - } - else - { - LOGGER.trace("Using cached list of server chat rooms."); - } - return Collections.unmodifiableList(list); - } - } - - /** - * Create a clean up job that checks the container after the cache has - * expired. If the container is still populated, then remove it. This clean - * up makes sure that there are no references left to an otherwise useless - * list of channels. - * - * @param channellist the container carrying the list of channel names - */ - private static void createCleanUpJob( - final Container<List<String>> channellist) - { - final Timer cleanUpJob = new Timer(); - final long timestamp = channellist.getTimestamp(); - cleanUpJob.schedule(new ChannelListCacheCleanUpTask(channellist, - timestamp), CHAT_ROOM_LIST_CACHE_EXPIRATION - / RATIO_MILLISECONDS_TO_NANOSECONDS + CACHE_CLEAN_UP_DELAY); - } - - /** - * Join a particular chat room. - * - * @param chatroom Chat room to join. - * @throws OperationFailedException failed to join the chat room - */ - public void join(final ChatRoomIrcImpl chatroom) - throws OperationFailedException - { - 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. - * @throws OperationFailedException failed to join the chat room - */ - public void join(final ChatRoomIrcImpl chatroom, final String password) - throws OperationFailedException - { - if (!isConnected()) - { - 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"); - } - - // Get instance of irc client api. - final IRCApi irc = this.session.get(); - if (irc == null) - { - throw new IllegalStateException("irc instance is not available"); - } - - final String chatRoomId = chatroom.getIdentifier(); - if (this.joined.containsKey(chatRoomId)) - { - // If we already joined this particular chatroom, no further action - // is required. - return; - } - - LOGGER.trace("Start joining channel " + chatRoomId); - final Result<Object, Exception> joinSignal = - new Result<Object, Exception>(); - synchronized (joinSignal) - { - LOGGER - .trace("Issue join channel command to IRC library and wait for" - + " join operation to complete (un)successfully."); - - this.joined.put(chatRoomId, null); - // TODO Refactor this ridiculous nesting of functions and - // classes. - irc.joinChannel(chatRoomId, password, new Callback<IRCChannel>() - { - - @Override - public void onSuccess(final IRCChannel channel) - { - if (LOGGER.isTraceEnabled()) - { - LOGGER.trace("Started callback for successful join " - + "of channel '" + chatroom.getIdentifier() + "'."); - } - boolean isRequestedChatRoom = - channel.getName().equalsIgnoreCase(chatRoomId); - synchronized (joinSignal) - { - if (!isRequestedChatRoom) - { - // We joined another chat room than the one we - // requested initially. - if (LOGGER.isTraceEnabled()) - { - LOGGER.trace("Callback for successful join " - + "finished prematurely since we " - + "got forwarded from '" + chatRoomId - + "' to '" + channel.getName() - + "'. Joining of forwarded channel " - + "gets handled by Server Listener " - + "since that channel was not " - + "announced."); - } - // Remove original chat room id from joined-list - // since we aren't actually attempting to join - // this room anymore. - IrcStack.this.joined.remove(chatRoomId); - IrcStack.this.provider - .getMUC() - .fireLocalUserPresenceEvent( - chatroom, - LocalUserChatRoomPresenceChangeEvent - .LOCAL_USER_JOIN_FAILED, - "We got forwarded to channel '" - + channel.getName() + "'."); - // Notify waiting threads of finished execution. - joinSignal.setDone(); - joinSignal.notifyAll(); - // The channel that we were forwarded to will be - // handled by the Server Listener, since the - // channel join was unannounced, and we are done - // here. - return; - } - - try - { - IrcStack.this.joined.put(chatRoomId, chatroom); - irc.addListener( - new ChatRoomListener(irc, chatroom)); - prepareChatRoom(chatroom, channel); - } - 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); - if (LOGGER.isTraceEnabled()) - { - LOGGER.trace("Finished successful join " - + "callback for channel '" + chatRoomId - + "'. Waking up original thread."); - } - // Notify waiting threads of finished - // execution. - joinSignal.setDone(); - joinSignal.notifyAll(); - } - } - } - - @Override - public void onFailure(final Exception e) - { - LOGGER.trace("Started callback for failed attempt to " - + "join channel '" + chatRoomId + "'."); - synchronized (joinSignal) - { - try - { - IrcStack.this.joined.remove(chatRoomId); - IrcStack.this.provider - .getMUC() - .fireLocalUserPresenceEvent( - chatroom, - LocalUserChatRoomPresenceChangeEvent - .LOCAL_USER_JOIN_FAILED, - e.getMessage()); - } - finally - { - if (LOGGER.isTraceEnabled()) - { - LOGGER.trace("Finished callback for failed " - + "attempt to join channel '" + chatRoomId - + "'. Waking up original thread."); - } - // Notify waiting threads of finished - // execution - joinSignal.setDone(e); - joinSignal.notifyAll(); - } - } - } - }); - - try - { - while (!joinSignal.isDone()) - { - LOGGER.trace("Waiting for channel join message ..."); - // Wait until async channel join operation has finished. - joinSignal.wait(); - } - - LOGGER - .trace("Finished waiting for join operation for channel '" - + chatroom.getIdentifier() + "' to complete."); - // TODO How to handle 480 (+j): Channel throttle exceeded? - } - catch (InterruptedException e) - { - LOGGER.error("Wait for join operation was interrupted.", e); - throw new OperationFailedException(e.getMessage(), - OperationFailedException.INTERNAL_ERROR, e); - } - } - } - - /** - * Part from a joined chat room. - * - * @param chatroom The chat room to part from. - */ - public void leave(final ChatRoomIrcImpl chatroom) - { - LOGGER.trace("Leaving chat room '" + chatroom.getIdentifier() + "'."); - leave(chatroom.getIdentifier()); - } - - /** - * Part from a joined chat room. - * - * @param chatRoomName The chat room to part from. - */ - private void leave(final String chatRoomName) - { - if (!isConnected()) - { - throw new IllegalStateException("Not connected to an IRC server."); - } - - final IRCApi irc = this.session.get(); - if (irc == null) - { - return; - } - try - { - irc.leaveChannel(chatRoomName); - } - catch (ApiException e) - { - LOGGER.warn("exception occurred while leaving channel", e); - } - } - - /** - * Ban chat room member. - * - * @param chatroom chat room to ban from - * @param member member to ban - * @param reason reason for banning - * @throws OperationFailedException throws operation failed in case of - * trouble. - */ - public void banParticipant(final ChatRoomIrcImpl chatroom, - final ChatRoomMember member, final String reason) - throws OperationFailedException - { - // TODO Implement banParticipant. - throw new OperationFailedException("Not implemented yet.", - OperationFailedException.NOT_SUPPORTED_OPERATION); - } - - /** - * Kick channel member. - * - * @param chatroom channel to kick from - * @param member member to kick - * @param reason kick message to deliver - */ - public void kickParticipant(final ChatRoomIrcImpl chatroom, - final ChatRoomMember member, final String reason) - { - if (!isConnected()) - { - return; - } - - final IRCApi irc = this.session.get(); - 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(final String memberId, final ChatRoomIrcImpl chatroom) - { - if (!isConnected()) - { - throw new IllegalStateException("Not connected to an IRC server."); - } - - final IRCApi irc = this.session.get(); - irc.rawMessage("INVITE " + memberId + " " + chatroom.getIdentifier()); - } - - /** - * Send a command to the IRC server. - * - * @param chatroom the chat room - * @param message the command message - */ - public void command(final ChatRoomIrcImpl chatroom, final String message) - { - this.command(chatroom.getIdentifier(), message); - } - - /** - * Send a command to the IRC server. - * - * @param contact the chat room - * @param message the command message - */ - public void command(final Contact contact, final MessageIrcImpl message) - { - this.command(contact.getAddress(), message.getContent()); - } - - /** - * Implementation of some commands. If the command is not recognized or - * implemented, it will be sent as if it were a normal message. - * - * TODO Eventually replace this with a factory such that we can easily - * extend with new commands. - * - * @param source Source contact or chat room from which the message is sent. - * @param message Command message that is sent. - */ - private void command(final String source, final String message) - { - if (!isConnected()) - { - throw new IllegalStateException("Not connected to IRC server."); - } - final IRCApi irc = this.session.get(); - final String msg = message.toLowerCase(); - if (msg.startsWith("/msg ")) - { - final String part = message.substring(5); - int endOfNick = part.indexOf(' '); - if (endOfNick == -1) - { - throw new IllegalArgumentException("Invalid private message " - + "format. Message was not sent."); - } - final String target = part.substring(0, endOfNick); - final String command = part.substring(endOfNick + 1); - irc.message(target, command); - } - else if (msg.startsWith("/me ")) - { - final String command = message.substring(4); - irc.act(source, command); - } - else if (msg.startsWith("/join ")) - { - final String part = message.substring(6); - final String channel; - final String password; - int indexOfSep = part.indexOf(' '); - if (indexOfSep == -1) - { - channel = part; - password = ""; - } - else - { - channel = part.substring(0, indexOfSep); - password = part.substring(indexOfSep + 1); - } - if (channel.matches("[^,\\n\\r\\s\\a]+")) - { - irc.joinChannel(channel, password); - } - } - else - { - irc.message(source, message); - } - } - - /** - * Send an IRC message. - * - * @param chatroom The chat room to send the message to. - * @param message The message to send. - */ - public void message(final ChatRoomIrcImpl chatroom, final String message) - { - if (!isConnected()) - { - throw new IllegalStateException("Not connected to an IRC server."); - } - final IRCApi irc = this.session.get(); - final String target = chatroom.getIdentifier(); - irc.message(target, message); - } - - /** - * Send an IRC message. - * - * @param contact The contact to send the message to. - * @param message The message to send. - */ - public void message(final Contact contact, final Message message) - { - if (!isConnected()) - { - throw new IllegalStateException("Not connected to an IRC server."); - } - final String target = contact.getAddress(); - try - { - final IRCApi irc = this.session.get(); - irc.message(target, message.getContent()); - LOGGER.trace("Message delivered to server successfully."); - } - catch (RuntimeException e) - { - LOGGER.trace("Failed to deliver message: " + e.getMessage(), e); - throw e; - } - } - - /** - * Check whether current state is away or online. - * - * @return returns true if away, or false if online - */ - public boolean isAway() - { - return isConnected() && this.presence.isAway(); - } - - /** - * Set or unset away message. In case the awayMessage is null the away - * message will be disabled and as a consequence the away-status is removed. - * - * @param away away status, <tt>true</tt> for away, <tt>false</tt> for - * available - * @param awayMessage the away message to set, or null to remove away-status - */ - public void away(final boolean away, final String awayMessage) - { - if (!isConnected()) - { - throw new IllegalStateException("Not connected to an IRC server."); - } - this.presence.away(away, awayMessage); - } - - /** - * Grant user permissions to specified user. - * - * @param chatRoom chat room to grant permissions for - * @param userAddress user to grant permissions to - * @param mode mode to grant - */ - public void grant(final ChatRoomIrcImpl chatRoom, final String userAddress, - final Mode mode) - { - if (!isConnected()) - { - throw new IllegalStateException("Not connected to an IRC server."); - } - if (mode.getRole() == null) - { - throw new IllegalArgumentException( - "This mode does not modify user permissions."); - } - final IRCApi irc = this.session.get(); - irc.changeMode(chatRoom.getIdentifier() + " +" + mode.getSymbol() + " " - + userAddress); - } - - /** - * Revoke user permissions of chat room for user. - * - * @param chatRoom chat room - * @param userAddress user - * @param mode mode - */ - public void revoke(final ChatRoomIrcImpl chatRoom, - final String userAddress, final Mode mode) - { - if (!isConnected()) - { - throw new IllegalStateException("Not connected to an IRC server."); - } - if (mode.getRole() == null) - { - throw new IllegalArgumentException( - "This mode does not modify user permissions."); - } - final IRCApi irc = this.session.get(); - irc.changeMode(chatRoom.getIdentifier() + " -" + mode.getSymbol() + " " - + userAddress); - } - - /** - * Prepare a chat room for initial opening. - * - * @param channel The IRC channel which is the source of data. - * @param chatRoom The chatroom to prepare. - */ - private void prepareChatRoom(final ChatRoomIrcImpl chatRoom, - final IRCChannel channel) - { - final IRCTopic topic = channel.getTopic(); - chatRoom.updateSubject(topic.getValue()); - - for (IRCUser user : channel.getUsers()) - { - ChatRoomMemberIrcImpl member = - new ChatRoomMemberIrcImpl(this.provider, chatRoom, - user.getNick(), ChatRoomMemberRole.SILENT_MEMBER); - ChatRoomMemberRole role; - for (IRCUserStatus status : channel.getStatusesForUser(user)) - { - role = convertMemberMode(status.getChanModeType().charValue()); - member.addRole(role); - } - chatRoom.addChatRoomMember(member.getContactAddress(), member); - if (this.getNick().equals(user.getNick())) - { - chatRoom.setLocalUser(member); - if (member.getRole() != ChatRoomMemberRole.SILENT_MEMBER) - { - ChatRoomLocalUserRoleChangeEvent event = - new ChatRoomLocalUserRoleChangeEvent(chatRoom, - ChatRoomMemberRole.SILENT_MEMBER, member.getRole(), - true); - chatRoom.fireLocalUserRoleChangedEvent(event); - } - } - } - } - - /** - * 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(final char modeSymbol) - { - return Mode.bySymbol(modeSymbol).getRole(); - } - - /** - * Calculate maximum message size that can be transmitted. - * - * @param contact receiving contact - * @return returns maximum message size - */ - public int calculateMaximumMessageSize(final Contact contact) - { - final String identity = getIdentityString(); - return IRC_PROTOCOL_MAXIMUM_MESSAGE_SIZE - - (":" + identity + " PRIVMSG " + contact.getAddress() + " :") - .length(); - } - - /** - * 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 final class ServerListener - extends VariousMessageListenerAdapter - { - /** - * IRC reply code for automatic reply containing away message. - */ - private static final int RPL_AWAY = 301; - - /** - * IRC error code for case of non-existing nick or channel name. - */ - private static final int ERR_NO_SUCH_NICK_CHANNEL = - IRCServerNumerics.NO_SUCH_NICK_CHANNEL; - - /** - * IRC reply code for end of list. - */ - private static final int RPL_LISTEND = - IRCServerNumerics.CHANNEL_NICKS_END_OF_LIST; - - /** - * Reply for WHOIS query. - */ - private static final int IRC_RPL_WHOISUSER = 311; - - /** - * IRCApi instance. - */ - private final IRCApi irc; - - /** - * Constructor for Server Listener. - * - * @param irc IRCApi instance - */ - private ServerListener(final IRCApi irc) - { - if (irc == null) - { - throw new IllegalArgumentException( - "irc instance cannot be null"); - } - this.irc = irc; - } - - /** - * Print out server notices for debugging purposes and for simply - * keeping track of the connections. - * - * @param msg the server notice - */ - @Override - public void onServerNotice(final ServerNotice msg) - { - LOGGER.debug("NOTICE: " + 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(final ServerNumericMessage msg) - { - if (LOGGER.isDebugEnabled()) - { - LOGGER.debug("NUM MSG: " + msg.getNumericCode() + ": " - + msg.getText()); - } - - Integer code = msg.getNumericCode(); - if (code == null) - { - LOGGER.debug("No 'code' in numeric message event."); - return; - } - - if (!IrcStack.this.isConnected()) - { - // Skip message handling until we're officially connected. - return; - } - - switch (code.intValue()) - { - case RPL_LISTEND: - // This indicates the end of a nick list as you will receive - // when joining a channel. This is used as the indicator that we - // have joined a channel. Now we have to determine whether or - // not we already know about this particular join attempt. If - // not, we continue to inform Jitsi and to create a listener - // for this new chat room. - final String text = msg.getText(); - final String channelName = text.substring(0, text.indexOf(' ')); - final ChatRoomIrcImpl chatRoom; - final IRCChannel channel; - synchronized (IrcStack.this.joined) - { - // Synchronize the section that checks then adds a chat - // room. This way we can be sure that there are no 2 - // simultaneous creation events. - if (IrcStack.this.joined.containsKey(channelName)) - { - LOGGER.trace("Chat room '" + channelName - + "' join event was announced or already " - + "finished. Stop handling this event."); - break; - } - // We aren't currently attempting to join, so this join is - // unannounced. - LOGGER.trace("Starting unannounced join of chat room '" - + channelName); - // Assuming that at the time that NICKS_END_OF_LIST is - // propagated, the channel join event has been completely - // handled by IRCApi. - channel = - IrcStack.this.connectionState - .getChannelByName(channelName); - chatRoom = new ChatRoomIrcImpl( - channelName, IrcStack.this.provider); - IrcStack.this.joined.put(channelName, chatRoom); - } - this.irc.addListener(new ChatRoomListener(this.irc, chatRoom)); - try - { - IrcStack.this.provider.getMUC() - .openChatRoomWindow(chatRoom); - } - catch (NullPointerException e) - { - LOGGER.error("failed to open chat room window", e); - } - IrcStack.this.prepareChatRoom(chatRoom, channel); - IrcStack.this.provider.getMUC().fireLocalUserPresenceEvent( - chatRoom, - LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_JOINED, - null); - LOGGER.trace("Unannounced join of chat room '" + channelName - + "' completed."); - break; - - case ERR_NO_SUCH_NICK_CHANNEL: - // TODO Check if target is Contact, then update contact presence - // status to off-line since the nick apparently does not exist - // anymore. - if (LOGGER.isTraceEnabled()) - { - LOGGER.trace("Message did not get delivered: " - + msg.asRaw()); - } - final String msgText = msg.getText(); - final int endOfTargetIndex = msgText.indexOf(' '); - if (endOfTargetIndex == -1) - { - LOGGER.trace("Expected target nick in error message, but " - + "it cannot be found. Stop parsing."); - break; - } - final String targetNick = - msgText.substring(0, endOfTargetIndex); - // Send blank text string as the message, since we don't know - // what the actual message was. (We cannot reliably relate the - // NOSUCHNICK reply to the exact message that caused the error.) - MessageIrcImpl message = - new MessageIrcImpl( - "", - OperationSetBasicInstantMessaging.HTML_MIME_TYPE, - OperationSetBasicInstantMessaging.DEFAULT_MIME_ENCODING, - null); - final Contact to = - IrcStack.this.provider.getPersistentPresence() - .findOrCreateContactByID(targetNick); - IrcStack.this.provider - .getBasicInstantMessaging() - .fireMessageDeliveryFailed( - message, - to, - MessageDeliveryFailedEvent - .OFFLINE_MESSAGES_NOT_SUPPORTED); - break; - - case RPL_AWAY: - final String rawAwayText = msg.getText(); - final String awayUserNick = - rawAwayText.substring(0, rawAwayText.indexOf(' ')); - final String awayText = - rawAwayText.substring(rawAwayText.indexOf(' ') + 2); - final MessageIrcImpl awayMessage = - MessageIrcImpl.newAwayMessageFromIRC(awayText); - final Contact awayUser = - IrcStack.this.provider.getPersistentPresence() - .findOrCreateContactByID(awayUserNick); - IrcStack.this.provider.getBasicInstantMessaging() - .fireMessageReceived(awayMessage, awayUser); - break; - - case IRC_RPL_WHOISUSER: - final String whoismsg = msg.getText(); - final int endNickIndex = whoismsg.indexOf(' '); - final String nick = whoismsg.substring(0, endNickIndex); - if (!IrcStack.this.connectionState.getNickname().equals(nick)) - { - // We need WHOIS info on ourselves to discover our identity - // on the IRC server. So skip other WHOIS replies. - return; - } - final int endUserIndex = - whoismsg.indexOf(' ', endNickIndex + 1); - final int endHostIndex = - whoismsg.indexOf(' ', endUserIndex + 1); - final String user = - whoismsg.substring(endNickIndex + 1, endUserIndex); - final String host = - whoismsg.substring(endUserIndex + 1, endHostIndex); - LOGGER.debug(String.format("Current identity: %s!%s@%s", - IrcStack.this.connectionState.getNickname(), user, host)); - IrcStack.this.identity = new IrcStack.Identity(user, host); - break; - - default: - if (LOGGER.isTraceEnabled()) - { - LOGGER.trace("This ServerNumericMessage (" + code - + ") will not be handled by the ServerListener."); - } - break; - } - } - - /** - * 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(final ErrorMessage msg) - { - if (LOGGER.isDebugEnabled()) - { - LOGGER - .debug("ERROR: " + msg.getSource() + ": " + msg.getText()); - } - if (IrcStack.this.connectionState != null) - { - if (!IrcStack.this.connectionState.isConnected()) - { - IrcStack.this.provider - .setCurrentRegistrationState( - RegistrationState.CONNECTION_FAILED); - } - } - } - - /** - * Upon receiving a private message from a user, deliver that to an - * instant messaging contact 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(final UserPrivMsg msg) - { - final String user = msg.getSource().getNick(); - final MessageIrcImpl message = - MessageIrcImpl.newMessageFromIRC(msg.getText()); - final Contact from = - IrcStack.this.provider.getPersistentPresence() - .findOrCreateContactByID(user); - try - { - IrcStack.this.provider.getBasicInstantMessaging() - .fireMessageReceived(message, from); - } - catch (RuntimeException e) - { - // TODO remove once this is stable. Don't want to lose message - // when an accidental error occurs. - // It is likely that errors occurred because of some issues with - // MetaContactGroup for NonPersistent group, since this is an - // outstanding error. - LOGGER.error( - "Error occurred while delivering private message from user" - + " '" + user + "': " + msg.getText(), e); - } - } - - /** - * Upon receiving a user notice message from a user, deliver that to an - * instant messaging contact. - * - * @param msg user notice message - */ - @Override - public void onUserNotice(final UserNotice msg) - { - final String user = msg.getSource().getNick(); - final Contact from = - IrcStack.this.provider.getPersistentPresence() - .findOrCreateContactByID(user); - final MessageIrcImpl message = - MessageIrcImpl.newNoticeFromIRC(from, msg.getText()); - IrcStack.this.provider.getBasicInstantMessaging() - .fireMessageReceived(message, from); - } - - /** - * Upon receiving a user action message from a user, deliver that to an - * instant messaging contact. - * - * @param msg user action message - */ - @Override - public void onUserAction(final UserActionMsg msg) - { - final String user = msg.getSource().getNick(); - final Contact from = - IrcStack.this.provider.getPersistentPresence().findContactByID( - user); - final MessageIrcImpl message = - MessageIrcImpl.newActionFromIRC(from, msg.getText()); - IrcStack.this.provider.getBasicInstantMessaging() - .fireMessageReceived(message, from); - } - - /** - * User quit messages. - * - * User quit messages only need to be handled in case quitting users, - * since that is the only clear signal of presence change we have. - * - * @param msg Quit message - */ - @Override - public void onUserQuit(final QuitMessage msg) - { - final String user = msg.getSource().getNick(); - if (user != null - && user.equals(IrcStack.this.connectionState.getNickname())) - { - LOGGER.debug("Local user's QUIT message received: removing " - + "server listener."); - this.irc.deleteListener(this); - return; - } - } - } - - /** - * A chat room listener. - * - * A chat room listener is registered for each chat room that we join. The - * chat room listener updates chat room data and fires events based on IRC - * messages that report state changes for the specified channel. - * - * @author Danny van Heumen - * - */ - private final class ChatRoomListener - extends VariousMessageListenerAdapter - { - /** - * IRC error code for case when user cannot send a message to the - * channel, for example when this channel is moderated and user does not - * have VOICE (+v). - */ - private static final int IRC_ERR_CANNOTSENDTOCHAN = 404; - - /** - * IRC error code for case where user is not joined to that channel. - */ - private static final int IRC_ERR_NOTONCHANNEL = 442; - - /** - * IRCApi instance. - */ - private final IRCApi irc; - - /** - * Chat room for which this listener is working. - */ - private final ChatRoomIrcImpl chatroom; - - /** - * Constructor. Instantiate listener for the provided chat room. - * - * @param irc IRCApi instance - * @param chatroom the chat room - */ - private ChatRoomListener(final IRCApi irc, - final ChatRoomIrcImpl chatroom) - { - if (chatroom == null) - { - throw new IllegalArgumentException("chatroom cannot be null"); - } - this.chatroom = chatroom; - if (irc == null) - { - throw new IllegalArgumentException("irc cannot be null"); - } - this.irc = irc; - } - - /** - * Event in case of topic change. - * - * @param msg topic change message - */ - @Override - public void onTopicChange(final TopicMessage msg) - { - if (!isThisChatRoom(msg.getChannelName())) - { - return; - } - this.chatroom.updateSubject(msg.getTopic().getValue()); - } - - /** - * Event in case of channel mode changes. - * - * @param msg channel mode message - */ - @Override - public void onChannelMode(final ChannelModeMessage msg) - { - if (!isThisChatRoom(msg.getChannelName())) - { - return; - } - - processModeMessage(msg); - } - - /** - * Event in case of channel join message. - * - * @param msg channel join message - */ - @Override - public void onChannelJoin(final ChanJoinMessage msg) - { - if (!isThisChatRoom(msg.getChannelName())) - { - return; - } - - final String user = msg.getSource().getNick(); - final ChatRoomMemberIrcImpl member = - new ChatRoomMemberIrcImpl(IrcStack.this.provider, - this.chatroom, user, ChatRoomMemberRole.SILENT_MEMBER); - this.chatroom.fireMemberPresenceEvent(member, null, - ChatRoomMemberPresenceChangeEvent.MEMBER_JOINED, null); - } - - /** - * Event in case of channel part. - * - * @param msg channel part message - */ - @Override - public void onChannelPart(final ChanPartMessage msg) - { - if (!isThisChatRoom(msg.getChannelName())) - { - return; - } - - final IRCUser user = msg.getSource(); - if (isMe(user)) - { - leaveChatRoom(); - return; - } - - final String userNick = msg.getSource().getNick(); - final ChatRoomMember member = - this.chatroom.getChatRoomMember(userNick); - if (member != null) - { - // When the account has been disabled, the chat room may return - // null. If that is NOT the case, continue handling. - try - { - this.chatroom.fireMemberPresenceEvent(member, null, - ChatRoomMemberPresenceChangeEvent.MEMBER_LEFT, - msg.getPartMsg()); - } - catch (NullPointerException e) - { - LOGGER.warn( - "This should not have happened. Please report this " - + "as it is a bug.", e); - } - } - } - - /** - * Some of the generic message are relevant to us, so keep an eye on - * general numeric messages. - * - * @param msg IRC server numeric message - */ - public void onServerNumericMessage(final ServerNumericMessage msg) - { - final Integer code = msg.getNumericCode(); - if (code == null) - { - return; - } - final String raw = msg.getText(); - switch (code.intValue()) - { - case IRC_ERR_NOTONCHANNEL: - final String channel = raw.substring(0, raw.indexOf(" ")); - if (isThisChatRoom(channel)) - { - LOGGER - .warn("Just discovered that we are no longer joined to " - + "channel " - + channel - + ". Leaving quietly. (This is most likely due to a" - + " bug in the implementation.)"); - // If for some reason we missed the message that we aren't - // joined (anymore) to this particular chat room, correct - // our problem ASAP. - leaveChatRoom(); - } - break; - - case IRC_ERR_CANNOTSENDTOCHAN: - final String cannotSendChannel = - raw.substring(0, raw.indexOf(" ")); - if (isThisChatRoom(cannotSendChannel)) - { - final MessageIrcImpl message = - new MessageIrcImpl("", "text/plain", "UTF-8", null); - this.chatroom.fireMessageDeliveryFailedEvent( - ChatRoomMessageDeliveryFailedEvent.FORBIDDEN, - "This channel is moderated.", new Date(), message); - } - break; - - default: - break; - } - } - - /** - * Event in case of channel kick. - * - * @param msg channel kick message - */ - @Override - public void onChannelKick(final ChannelKick msg) - { - if (!isThisChatRoom(msg.getChannelName())) - { - return; - } - - if (!IrcStack.this.isConnected()) - { - LOGGER.error("Not currently connected to IRC Server. " - + "Aborting message handling."); - return; - } - - final String kickedUser = msg.getKickedNickname(); - final ChatRoomMember kickedMember = - this.chatroom.getChatRoomMember(kickedUser); - final String user = msg.getSource().getNick(); - if (kickedMember != null) - { - ChatRoomMember kicker = this.chatroom.getChatRoomMember(user); - this.chatroom.fireMemberPresenceEvent(kickedMember, kicker, - ChatRoomMemberPresenceChangeEvent.MEMBER_KICKED, - msg.getText()); - } - if (isMe(kickedUser)) - { - LOGGER.debug( - "Local user is kicked. Removing chat room listener."); - this.irc.deleteListener(this); - IrcStack.this.joined.remove(this.chatroom.getIdentifier()); - IrcStack.this.provider.getMUC().fireLocalUserPresenceEvent( - this.chatroom, - LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_KICKED, - msg.getText()); - } - } - - /** - * Event in case of user quit. - * - * @param msg user quit message - */ - @Override - public void onUserQuit(final QuitMessage msg) - { - String user = msg.getSource().getNick(); - if (user == null) - { - return; - } - if (user.equals(IrcStack.this.connectionState.getNickname())) - { - LOGGER.debug("Local user QUIT message received: removing chat " - + "room listener."); - this.irc.deleteListener(this); - return; - } - final ChatRoomMember member = this.chatroom.getChatRoomMember(user); - if (member != null) - { - this.chatroom.fireMemberPresenceEvent(member, null, - ChatRoomMemberPresenceChangeEvent.MEMBER_QUIT, - msg.getQuitMsg()); - } - } - - /** - * Event in case of nick change. - * - * @param msg nick change message - */ - @Override - public void onNickChange(final NickMessage msg) - { - if (msg == null) - { - return; - } - - final String oldNick = msg.getSource().getNick(); - final String newNick = msg.getNewNick(); - - final ChatRoomMemberIrcImpl member = - (ChatRoomMemberIrcImpl) this.chatroom - .getChatRoomMember(oldNick); - if (member != null) - { - member.setName(newNick); - this.chatroom.updateChatRoomMemberName(oldNick); - ChatRoomMemberPropertyChangeEvent evt = - new ChatRoomMemberPropertyChangeEvent(member, - this.chatroom, - ChatRoomMemberPropertyChangeEvent.MEMBER_NICKNAME, - oldNick, newNick); - this.chatroom.fireMemberPropertyChangeEvent(evt); - } - } - - /** - * Event in case of channel message arrival. - * - * @param msg channel message - */ - @Override - public void onChannelMessage(final ChannelPrivMsg msg) - { - if (!isThisChatRoom(msg.getChannelName())) - { - return; - } - - final MessageIrcImpl message = - MessageIrcImpl.newMessageFromIRC(msg.getText()); - final ChatRoomMemberIrcImpl member = - new ChatRoomMemberIrcImpl(IrcStack.this.provider, - this.chatroom, msg.getSource().getNick(), - ChatRoomMemberRole.MEMBER); - this.chatroom.fireMessageReceivedEvent(message, member, new Date(), - ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED); - } - - /** - * Event in case of channel action message arrival. - * - * @param msg channel action message - */ - @Override - public void onChannelAction(final ChannelActionMsg msg) - { - if (!isThisChatRoom(msg.getChannelName())) - { - return; - } - - String userNick = msg.getSource().getNick(); - ChatRoomMemberIrcImpl member = - new ChatRoomMemberIrcImpl(IrcStack.this.provider, - this.chatroom, userNick, ChatRoomMemberRole.MEMBER); - MessageIrcImpl message = - MessageIrcImpl.newActionFromIRC(member, msg.getText()); - this.chatroom.fireMessageReceivedEvent(message, member, new Date(), - ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED); - } - - /** - * Event in case of channel notice message arrival. - * - * @param msg channel notice message - */ - @Override - public void onChannelNotice(final ChannelNotice msg) - { - if (!isThisChatRoom(msg.getChannelName())) - { - return; - } - - final String userNick = msg.getSource().getNick(); - final ChatRoomMemberIrcImpl member = - new ChatRoomMemberIrcImpl(IrcStack.this.provider, - this.chatroom, userNick, ChatRoomMemberRole.MEMBER); - final MessageIrcImpl message = - MessageIrcImpl.newNoticeFromIRC(member, msg.getText()); - this.chatroom.fireMessageReceivedEvent(message, member, new Date(), - ChatRoomMessageReceivedEvent.CONVERSATION_MESSAGE_RECEIVED); - } - - /** - * Leave this chat room. - */ - private void leaveChatRoom() - { - this.irc.deleteListener(this); - IrcStack.this.joined.remove(this.chatroom.getIdentifier()); - LOGGER.debug("Leaving chat room " + this.chatroom.getIdentifier() - + ". Chat room listener removed."); - IrcStack.this.provider.getMUC().fireLocalUserPresenceEvent( - this.chatroom, - LocalUserChatRoomPresenceChangeEvent.LOCAL_USER_LEFT, null); - } - - /** - * Process mode changes. - * - * @param msg raw mode message - */ - private void processModeMessage(final ChannelModeMessage msg) - { - final ChatRoomMemberIrcImpl source = extractChatRoomMember(msg); - final ModeParser parser = new ModeParser(msg.getModeStr()); - for (ModeEntry mode : parser.getModes()) - { - switch (mode.getMode()) - { - case OWNER: - case OPERATOR: - case HALFOP: - case VOICE: - processRoleChange(source, mode); - break; - case LIMIT: - processLimitChange(source, mode); - break; - case BAN: - processBanChange(source, mode); - break; - case UNKNOWN: - if (LOGGER.isInfoEnabled()) - { - LOGGER.info("Unknown mode: " - + (mode.isAdded() ? "+" : "-") - + mode.getParams()[0] + ". Original mode string: '" - + msg.getModeStr() + "'"); - } - break; - default: - if (LOGGER.isInfoEnabled()) - { - LOGGER.info("Unsupported mode '" - + (mode.isAdded() ? "+" : "-") + mode.getMode() - + "' (from modestring '" + msg.getModeStr() + "')"); - } - break; - } - } - } - - /** - * Process changes for ban patterns. - * - * @param sourceMember the originating member - * @param mode the ban mode change - */ - private void processBanChange(final ChatRoomMemberIrcImpl sourceMember, - final ModeEntry mode) - { - final MessageIrcImpl banMessage = - new MessageIrcImpl( - "channel ban mask was " - + (mode.isAdded() ? "added" : "removed") - + ": " - + mode.getParams()[0] - + " by " - + (sourceMember.getContactAddress().length() == 0 - ? "server" - : sourceMember.getContactAddress()), - MessageIrcImpl.DEFAULT_MIME_TYPE, - MessageIrcImpl.DEFAULT_MIME_ENCODING, null); - this.chatroom.fireMessageReceivedEvent(banMessage, sourceMember, - new Date(), - ChatRoomMessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED); - } - - /** - * Process mode changes resulting in role manipulation. - * - * @param sourceMember the originating member - * @param mode the mode change - */ - private void processRoleChange( - final ChatRoomMemberIrcImpl sourceMember, final ModeEntry mode) - { - final String targetNick = mode.getParams()[0]; - final ChatRoomMemberIrcImpl targetMember = - (ChatRoomMemberIrcImpl) this.chatroom - .getChatRoomMember(targetNick); - final ChatRoomMemberRole originalRole = targetMember.getRole(); - if (mode.isAdded()) - { - targetMember.addRole(mode.getMode().getRole()); - } - else - { - targetMember.removeRole(mode.getMode().getRole()); - } - final ChatRoomMemberRole newRole = targetMember.getRole(); - if (newRole != originalRole) - { - // Mode change actually caused a role change. - final ChatRoomLocalUserRoleChangeEvent event = - new ChatRoomLocalUserRoleChangeEvent(this.chatroom, - originalRole, newRole, false); - if (isMe(targetMember.getContactAddress())) - { - this.chatroom.fireLocalUserRoleChangedEvent(event); - } - else - { - this.chatroom.fireMemberRoleEvent(targetMember, - newRole); - } - } - else - { - // Mode change did not cause an immediate role change. - // Display a system message for the mode change. - final String text = - sourceMember.getName() - + (mode.isAdded() ? " gives " - + mode.getMode().name().toLowerCase() - + " to " : " removes " - + mode.getMode().name().toLowerCase() - + " from ") + targetMember.getName(); - final MessageIrcImpl message = - new MessageIrcImpl(text, - MessageIrcImpl.DEFAULT_MIME_TYPE, - MessageIrcImpl.DEFAULT_MIME_ENCODING, null); - this.chatroom - .fireMessageReceivedEvent( - message, - sourceMember, - new Date(), - ChatRoomMessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED); - } - } - - /** - * Process mode change that represents a channel limit modification. - * - * @param sourceMember the originating member - * @param mode the limit mode change - */ - private void processLimitChange( - final ChatRoomMemberIrcImpl sourceMember, final ModeEntry mode) - { - final MessageIrcImpl limitMessage; - if (mode.isAdded()) - { - try - { - limitMessage = - new MessageIrcImpl( - "channel limit set to " - + Integer.parseInt(mode.getParams()[0]) - + " by " - + (sourceMember.getContactAddress() - .length() == 0 - ? "server" - : sourceMember.getContactAddress()), - "text/plain", "UTF-8", null); - } - catch (NumberFormatException e) - { - LOGGER.warn("server sent incorrect limit: " - + "limit is not a number", e); - return; - } - } - else - { - // TODO "server" is now easily fakeable if someone - // calls himself server. There should be some other way - // to represent the server if a message comes from - // something other than a normal chat room member. - limitMessage = - new MessageIrcImpl( - "channel limit removed by " - + (sourceMember.getContactAddress().length() == 0 - ? "server" - : sourceMember.getContactAddress()), - "text/plain", "UTF-8", null); - } - this.chatroom.fireMessageReceivedEvent(limitMessage, sourceMember, - new Date(), - ChatRoomMessageReceivedEvent.SYSTEM_MESSAGE_RECEIVED); - } - - /** - * Extract chat room member identifier from message. - * - * @param msg raw mode message - * @return returns member instance - */ - private ChatRoomMemberIrcImpl extractChatRoomMember( - final ChannelModeMessage msg) - { - ChatRoomMemberIrcImpl member; - ISource source = msg.getSource(); - if (source instanceof IRCServer) - { - // TODO Created chat room member with creepy empty contact ID. - // Interacting with this contact might screw up other sections - // of code which is not good. Is there a better way to represent - // an IRC server as a chat room member? - member = - new ChatRoomMemberIrcImpl(IrcStack.this.provider, - this.chatroom, "", ChatRoomMemberRole.ADMINISTRATOR); - } - else if (source instanceof IRCUser) - { - String nick = ((IRCUser) source).getNick(); - member = - (ChatRoomMemberIrcImpl) this.chatroom - .getChatRoomMember(nick); - } - else - { - throw new IllegalArgumentException("Unknown source type: " - + source.getClass().getName()); - } - return member; - } - - /** - * Test whether this listener corresponds to the chat room. - * - * @param chatRoomName chat room name - * @return returns true if this listener applies, false otherwise - */ - private boolean isThisChatRoom(final String chatRoomName) - { - return this.chatroom.getIdentifier().equalsIgnoreCase(chatRoomName); - } - - /** - * Test whether the source user is this user. - * - * @param user the source user - * @return returns true if this use, or false otherwise - */ - private boolean isMe(final IRCUser user) - { - return isMe(user.getNick()); - } - - /** - * Test whether the user nick is this user. - * - * @param name nick of the user - * @return returns true if so, false otherwise - */ - private boolean isMe(final String name) - { - final String userNick = IrcStack.this.connectionState.getNickname(); - if (userNick == null) - { - return false; - } - return userNick.equals(name); - } - } - - /** - * Special listener that processes LIST replies and signals once the list is - * completely filled. - */ - private static final class ChannelListListener - extends VariousMessageListenerAdapter - { - /** - * Start of an IRC server channel listing reply. - */ - private static final int RPL_LISTSTART = 321; - - /** - * Continuation of an IRC server channel listing reply. - */ - private static final int RPL_LIST = 322; - - /** - * End of an IRC server channel listing reply. - */ - private static final int RPL_LISTEND = 323; - - /** - * Reference to the IRC API instance. - */ - private final IRCApi api; - - /** - * Reference to the provided list instance. - */ - private final Result<List<String>, Exception> signal; - - /** - * Constructor for channel list listener. - * - * @param api irc-api library instance - * @param signal signal for sync signaling - */ - private ChannelListListener(final IRCApi api, - final Result<List<String>, Exception> signal) - { - if (api == null) - { - throw new IllegalArgumentException( - "IRC api instance cannot be null"); - } - this.api = api; - this.signal = signal; - } - - /** - * Act on LIST messages: 321 RPL_LISTSTART, 322 RPL_LIST, 323 - * RPL_LISTEND - * - * Clears the list upon starting. All received channels are added to the - * list. Upon receiving RPL_LISTEND finalize the list and signal the - * waiting thread that it can continue processing the list. - * - * @param msg The numeric server message. - */ - @Override - public void onServerNumericMessage(final ServerNumericMessage msg) - { - if (this.signal.isDone()) - { - return; - } - - switch (msg.getNumericCode()) - { - case RPL_LISTSTART: - // TODO According to RFC2812 this message is obsolete and not in - // use anymore. Shouldn't be much of a problem, since we usually - // start with an empty list though. - synchronized (this.signal) - { - this.signal.getValue().clear(); - } - break; - case RPL_LIST: - String channel = parse(msg.getText()); - if (channel != null) - { - synchronized (this.signal) - { - this.signal.getValue().add(channel); - } - } - break; - case RPL_LISTEND: - synchronized (this.signal) - { - // Done collecting channels. Remove listener and then we're - // done. - this.api.deleteListener(this); - this.signal.setDone(); - this.signal.notifyAll(); - } - break; - // TODO Add support for REPLY 416: LIST :output too large, truncated - default: - break; - } - } - - /** - * Parse an IRC server response RPL_LIST. Extract the channel name. - * - * @param text raw server response - * @return returns the channel name - */ - private String parse(final String text) - { - int endOfChannelName = text.indexOf(' '); - if (endOfChannelName == -1) - { - return null; - } - // Create a new string to make sure that the original (larger) - // strings can be GC'ed. - return new String(text.substring(0, endOfChannelName)); - } - } - - /** * Listener for debugging purposes. If logging level is set high enough, * this listener is added to the irc-api client so it can show all IRC * messages as they are handled. @@ -2370,11 +257,16 @@ public class IrcStack * Reserved symbols. These symbols have special meaning and cannot be * used to start nick names. */ - private static final Set<Character> RESERVED = new HashSet<Character>(); + private static final Set<Character> RESERVED; + /** + * Initialize RESERVED symbols set. + */ static { - RESERVED.add('#'); - RESERVED.add('&'); + final HashSet<Character> reserved = new HashSet<Character>(); + reserved.add('#'); + reserved.add('&'); + RESERVED = Collections.unmodifiableSet(reserved); } /** @@ -2453,7 +345,7 @@ public class IrcStack * @param nick nick name * @return returns nick name */ - private String checkNick(final String nick) + private static String checkNick(final String nick) { if (nick == null) { @@ -2528,181 +420,4 @@ public class IrcStack this.server = server; } } - - /** - * Simplest possible container that we can use for locking while we're - * checking/modifying the contents. - * - * @param <T> The type of instance to store in the container - */ - private static final class Container<T> - { - /** - * The stored instance. (Can be null) - */ - private T instance; - - /** - * Time of stored instance. - */ - private long time; - - /** - * Constructor that immediately sets the instance. - * - * @param instance the instance to set - */ - private Container(final T instance) - { - this.instance = instance; - this.time = System.nanoTime(); - } - - /** - * Conditionally get the stored instance. Get the instance when time - * difference is within specified bound. Otherwise return null. - * - * @param bound maximum time difference that is allowed. - * @return returns instance if within bounds, or null otherwise - */ - public T get(final long bound) - { - if (System.nanoTime() - this.time > bound) - { - return null; - } - return this.instance; - } - - /** - * Set an instance. - * - * @param instance the instance - */ - public void set(final T instance) - { - this.instance = instance; - this.time = System.nanoTime(); - } - - /** - * Get the timestamp from when the instance was set. - * - * @return returns the timestamp - */ - public long getTimestamp() - { - return this.time; - } - } - - /** - * Task for cleaning up old channel list caches. - * - * @author Danny van Heumen - */ - private static final class ChannelListCacheCleanUpTask - extends TimerTask - { - /** - * Expected timestamp on which the list cache was created. It is used as - * an indicator to see whether the cache has been refreshed in the mean - * time. - */ - private final long timestamp; - - /** - * Container holding the channel list cache. - */ - private final Container<List<String>> container; - - /** - * Construct new clean up job definition. - * - * @param listContainer container that holds the channel list cache - * @param timestamp expected timestamp of list cache creation - */ - private ChannelListCacheCleanUpTask( - final Container<List<String>> listContainer, final long timestamp) - { - if (listContainer == null) - { - throw new IllegalArgumentException( - "listContainer cannot be null"); - } - this.container = listContainer; - this.timestamp = timestamp; - } - - /** - * Remove the list reference from the container. But only if the - * timestamp matches. This makes sure that only one clean up job will - * clean up a list. - */ - @Override - public void run() - { - synchronized (this.container) - { - // Only clean up old cache if this is the dedicated task for it. - // If the timestamp has changed, another job is responsible for - // the clean up. - if (this.container.getTimestamp() != this.timestamp) - { - LOGGER.trace("Not cleaning up channel list cache. The " - + "timestamp does not match."); - return; - } - this.container.set(null); - } - // We cannot clear the list itself, since the contents might still - // be in use by the UI, inside the immutable wrapper. - LOGGER.debug("Old channel list cache has been cleared."); - } - } - - /** - * Storage container for identity components. - * - * IRC identity components user and host are stored. The nick name component - * isn't stored, because it changes too frequently. When getting the - * identity string, the nick name component is provided at calling time. - * - * @author Danny van Heumen - */ - private static final class Identity - { - /** - * User name. - */ - private final String user; - - /** - * Host name. - */ - private final String host; - - /** - * Constructor. - * - * @param user user - * @param host host - */ - private Identity(final String user, final String host) - { - this.user = user; - this.host = host; - } - - /** - * Get identity string. - * - * @param currentNick the current nick - * @return returns identity string - */ - public String getIdentityString(final String currentNick) - { - return String.format("%s!%s@%s", currentNick, this.user, this.host); - } - } -} +}
\ No newline at end of file diff --git a/src/net/java/sip/communicator/impl/protocol/irc/OperationSetBasicInstantMessagingIrcImpl.java b/src/net/java/sip/communicator/impl/protocol/irc/OperationSetBasicInstantMessagingIrcImpl.java index 3154e18..d8296d1 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/OperationSetBasicInstantMessagingIrcImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/OperationSetBasicInstantMessagingIrcImpl.java @@ -1,7 +1,12 @@ /* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * +<<<<<<< HEAD * Distributable under LGPL license. See terms of license at gnu.org. +======= + * Distributable under LGPL license. + * See terms of license at gnu.org. +>>>>>>> Restructured and extract IrcConnection type for better separation of */ package net.java.sip.communicator.impl.protocol.irc; @@ -108,19 +113,21 @@ public class OperationSetBasicInstantMessagingIrcImpl // FIXME how to handle HTML content? // Note: can't set subject since it leaks information while - // message content actually gets encoded. + // message content actually gets encrypted. MessageIrcImpl message = this.createMessage(transformedContent, original.getContentType(), original.getEncoding(), ""); + final IrcConnection connection = + this.provider.getIrcStack().getConnection(); try { if (!event.isMessageEncrypted() && message.isCommand()) { - this.provider.getIrcStack().command(to, message); + connection.command(to, message); } else { - this.provider.getIrcStack().message(to, message); + connection.message(to, message); } } catch (RuntimeException e) @@ -215,7 +222,13 @@ public class OperationSetBasicInstantMessagingIrcImpl @Override public int getMaxMessageSize(final Contact contact) { - return this.provider.getIrcStack().calculateMaximumMessageSize(contact); + IrcConnection connection = this.provider.getIrcStack().getConnection(); + if (connection == null) + { + // FIXME how to handle this? + return 0; + } + return connection.calculateMaximumMessageSize(contact); } /** diff --git a/src/net/java/sip/communicator/impl/protocol/irc/OperationSetMultiUserChatIrcImpl.java b/src/net/java/sip/communicator/impl/protocol/irc/OperationSetMultiUserChatIrcImpl.java index c45c8b3..cf9aa68 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/OperationSetMultiUserChatIrcImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/OperationSetMultiUserChatIrcImpl.java @@ -71,7 +71,9 @@ public class OperationSetMultiUserChatIrcImpl */ public List<String> getExistingChatRooms() throws OperationFailedException { - return ircProvider.getIrcStack().getServerChatRoomList(); + final IrcConnection connection = + this.ircProvider.getIrcStack().getConnection(); + return connection.getServerChatRoomList(); } /** @@ -179,7 +181,7 @@ public class OperationSetMultiUserChatIrcImpl String message = IrcActivator.getResources().getI18NString( "service.gui.CREATE_CHAT_ROOM_ERROR", new String[] - { roomName }); + {roomName}); throw new OperationFailedException(message, OperationFailedException.ILLEGAL_ARGUMENT, e); } diff --git a/src/net/java/sip/communicator/impl/protocol/irc/OperationSetPersistentPresenceIrcImpl.java b/src/net/java/sip/communicator/impl/protocol/irc/OperationSetPersistentPresenceIrcImpl.java index a5e3771..27042c4 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/OperationSetPersistentPresenceIrcImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/OperationSetPersistentPresenceIrcImpl.java @@ -311,10 +311,11 @@ public class OperationSetPersistentPresenceIrcImpl @Override public PresenceStatus getPresenceStatus() { - if (this.parentProvider.getIrcStack().isConnected()) + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + if (connection != null && connection.isConnected()) { - return this.parentProvider.getIrcStack().isAway() - ? IrcStatusEnum.AWAY + return connection.isAway() ? IrcStatusEnum.AWAY : IrcStatusEnum.ONLINE; } else @@ -336,19 +337,25 @@ public class OperationSetPersistentPresenceIrcImpl IllegalStateException, OperationFailedException { + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + if (connection == null) + { + return; + } if (statusMessage != null && statusMessage.isEmpty()) { // if we provide a message, make sure it isn't empty statusMessage = null; } - final IrcStack provider = this.parentProvider.getIrcStack(); + if (status.getStatus() >= IrcStatusEnum.AVAILABLE_THRESHOLD) { - provider.away(false, statusMessage); + connection.away(false, statusMessage); } else if (status.getStatus() >= IrcStatusEnum.AWAY_THRESHOLD) { - provider.away(true, statusMessage); + connection.away(true, statusMessage); } else { @@ -467,7 +474,12 @@ public class OperationSetPersistentPresenceIrcImpl { // FIXME look up active status message in case of away or "" in case of // available. - return this.parentProvider.getIrcStack().isAway() ? "" : ""; + IrcConnection connection = this.parentProvider.getIrcStack().getConnection(); + if (connection == null) + { + return ""; + } + return connection.isAway() ? "" : ""; } /** diff --git a/src/net/java/sip/communicator/impl/protocol/irc/ProtocolProviderServiceIrcImpl.java b/src/net/java/sip/communicator/impl/protocol/irc/ProtocolProviderServiceIrcImpl.java index bd6cf84..84a65e5 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/ProtocolProviderServiceIrcImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/ProtocolProviderServiceIrcImpl.java @@ -364,15 +364,16 @@ public class ProtocolProviderServiceIrcImpl public void unregister() throws OperationFailedException { - for (ChatRoom joinedChatRoom - : multiUserChat.getCurrentlyJoinedChatRooms()) + for (ChatRoom joinedChatRoom : multiUserChat + .getCurrentlyJoinedChatRooms()) { joinedChatRoom.leave(); } - if (ircStack.isConnected()) + final IrcConnection connection = this.ircStack.getConnection(); + if (connection != null) { - ircStack.disconnect(); + this.ircStack.disconnect(); } } @@ -385,7 +386,8 @@ public class ProtocolProviderServiceIrcImpl @Override public boolean isSignalingTransportSecure() { - return this.ircStack.isSecureConnection(); + final IrcConnection connection = this.ircStack.getConnection(); + return connection != null && connection.isSecureConnection(); } /** diff --git a/test/net/java/sip/communicator/impl/protocol/irc/ChatRoomIrcImplTest.java b/test/net/java/sip/communicator/impl/protocol/irc/ChatRoomIrcImplTest.java index ef18049..a057306 100644 --- a/test/net/java/sip/communicator/impl/protocol/irc/ChatRoomIrcImplTest.java +++ b/test/net/java/sip/communicator/impl/protocol/irc/ChatRoomIrcImplTest.java @@ -15,6 +15,7 @@ public class ChatRoomIrcImplTest { private ProtocolProviderServiceIrcImpl providerMock; private IrcStack stackMock; + private IrcConnection connectionMock; //@before public void setUp() throws Exception @@ -23,15 +24,17 @@ public class ChatRoomIrcImplTest this.providerMock = EasyMock.createMock(ProtocolProviderServiceIrcImpl.class); this.stackMock = EasyMock.createMock(IrcStack.class); + this.connectionMock = EasyMock.createMock(IrcConnection.class); EasyMock.expect(this.providerMock.getIrcStack()).andReturn(stackMock); - EasyMock.expect(this.stackMock.getChannelTypes()).andReturn( + EasyMock.expect(this.stackMock.getConnection()).andReturn(this.connectionMock); + EasyMock.expect(this.connectionMock.getChannelTypes()).andReturn( Collections.unmodifiableSet(Sets.newHashSet('#', '&'))); } //@Test public void testConstruction() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); new ChatRoomIrcImpl("#test", this.providerMock); } @@ -99,7 +102,7 @@ public class ChatRoomIrcImplTest //@Test public void testAutoPrefixBadChannelName() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("!test", this.providerMock); Assert.assertEquals("#!test", room.getIdentifier()); } @@ -107,7 +110,7 @@ public class ChatRoomIrcImplTest //@Test(expected = IllegalArgumentException.class) public void testIllegalNameSpace() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); try { new ChatRoomIrcImpl("#test test", this.providerMock); @@ -121,7 +124,7 @@ public class ChatRoomIrcImplTest //@Test(expected = IllegalArgumentException.class) public void testIllegalNameComma() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); try { new ChatRoomIrcImpl("#test,test", this.providerMock); @@ -135,14 +138,14 @@ public class ChatRoomIrcImplTest //@Test public void testValidName() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); } //@Test public void testCorrectConstruction() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertEquals("#my-cool-channel", room.getIdentifier()); @@ -153,7 +156,7 @@ public class ChatRoomIrcImplTest //@Test public void testHashCodeNotFailing() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); room.hashCode(); @@ -163,13 +166,15 @@ public class ChatRoomIrcImplTest public void testRoomIsJoined() { EasyMock.expect(this.providerMock.getIrcStack()) - .andReturn(this.stackMock).andReturn(this.stackMock); + .andReturn(this.stackMock).times(2); + EasyMock.expect(this.stackMock.getConnection()).andReturn( + this.connectionMock).times(2); EasyMock .expect( - this.stackMock.isJoined(EasyMock + this.connectionMock.isJoined(EasyMock .anyObject(ChatRoomIrcImpl.class))).andReturn(false) .andReturn(true); - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertFalse(room.isJoined()); @@ -179,7 +184,7 @@ public class ChatRoomIrcImplTest //@Test public void testIsPersistentRoom() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertTrue(room.isPersistent()); @@ -188,7 +193,7 @@ public class ChatRoomIrcImplTest //@Test public void testDestroyRoom() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertTrue(room.destroy("whatever", null)); @@ -197,7 +202,7 @@ public class ChatRoomIrcImplTest //@Test public void testSetLocalUserNull() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); try @@ -213,7 +218,7 @@ public class ChatRoomIrcImplTest //@Test public void testSetLocalUser() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertEquals(ChatRoomMemberRole.SILENT_MEMBER, @@ -238,7 +243,8 @@ public class ChatRoomIrcImplTest { ChatRoomMemberIrcImpl user = EasyMock.createMock(ChatRoomMemberIrcImpl.class); - EasyMock.replay(this.providerMock, this.stackMock, user); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock, + user); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertEquals(0, room.getMembersCount()); @@ -256,7 +262,8 @@ public class ChatRoomIrcImplTest { ChatRoomMemberIrcImpl user = EasyMock.createMock(ChatRoomMemberIrcImpl.class); - EasyMock.replay(this.providerMock, this.stackMock, user); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock, + user); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertNull(room.getChatRoomMember("user")); @@ -271,7 +278,8 @@ public class ChatRoomIrcImplTest { ChatRoomMemberIrcImpl user = EasyMock.createMock(ChatRoomMemberIrcImpl.class); - EasyMock.replay(this.providerMock, this.stackMock, user); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock, + user); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); room.addChatRoomMember("user", user); @@ -285,7 +293,7 @@ public class ChatRoomIrcImplTest //@Test public void testEqualsSame() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertTrue(room.equals(room)); @@ -294,7 +302,7 @@ public class ChatRoomIrcImplTest //@Test public void testEqualsNull() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertFalse(room.equals(null)); @@ -303,7 +311,7 @@ public class ChatRoomIrcImplTest //@Test public void testEqualsOtherClassInstance() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertFalse(room.equals(new Object())); @@ -315,9 +323,12 @@ public class ChatRoomIrcImplTest ProtocolProviderServiceIrcImpl providerMock2 = EasyMock.createMock(ProtocolProviderServiceIrcImpl.class); EasyMock.expect(providerMock2.getIrcStack()).andReturn(this.stackMock); - EasyMock.expect(this.stackMock.getChannelTypes()).andReturn( + EasyMock.expect(this.stackMock.getConnection()).andReturn( + this.connectionMock); + EasyMock.expect(this.connectionMock.getChannelTypes()).andReturn( Collections.unmodifiableSet(Sets.newHashSet('#', '$'))); - EasyMock.replay(this.providerMock, this.stackMock, providerMock2); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock, + providerMock2); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); ChatRoomIrcImpl room2 = @@ -329,9 +340,11 @@ public class ChatRoomIrcImplTest public void testEqualsOtherRoomInstance() { EasyMock.expect(this.providerMock.getIrcStack()).andReturn(stackMock); - EasyMock.expect(this.stackMock.getChannelTypes()).andReturn( + EasyMock.expect(this.stackMock.getConnection()).andReturn( + this.connectionMock); + EasyMock.expect(this.connectionMock.getChannelTypes()).andReturn( Collections.unmodifiableSet(Sets.newHashSet('#', '$'))); - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); ChatRoomIrcImpl room2 = @@ -343,9 +356,11 @@ public class ChatRoomIrcImplTest public void testEqualsSameRoomRepresentation() { EasyMock.expect(this.providerMock.getIrcStack()).andReturn(stackMock); - EasyMock.expect(this.stackMock.getChannelTypes()).andReturn( + EasyMock.expect(this.stackMock.getConnection()).andReturn( + this.connectionMock); + EasyMock.expect(this.connectionMock.getChannelTypes()).andReturn( Collections.unmodifiableSet(Sets.newHashSet('#', '$'))); - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); ChatRoomIrcImpl room2 = @@ -356,7 +371,7 @@ public class ChatRoomIrcImplTest //@Test public void testGetChatRoomSubject() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); Assert.assertEquals("", room.getSubject()); @@ -366,15 +381,17 @@ public class ChatRoomIrcImplTest public void testSetChatRoomSubject() throws OperationFailedException { final String newSubject = "My test subject!"; - this.stackMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), + this.connectionMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), EasyMock.eq(newSubject)); EasyMock.expectLastCall(); EasyMock.expect(this.providerMock.getIrcStack()).andReturn( this.stackMock); - this.stackMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), + EasyMock.expect(this.stackMock.getConnection()).andReturn( + this.connectionMock); + this.connectionMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), EasyMock.eq(newSubject)); EasyMock.expectLastCall(); - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); @@ -391,16 +408,18 @@ public class ChatRoomIrcImplTest throws OperationFailedException { final String newSubject = "My test subject!"; - this.stackMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), + this.connectionMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), EasyMock.eq(newSubject)); EasyMock.expectLastCall().andThrow( new RuntimeException("Some error", new IOException("Real cause"))); EasyMock.expect(this.providerMock.getIrcStack()).andReturn( this.stackMock); - this.stackMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), + EasyMock.expect(this.stackMock.getConnection()).andReturn( + this.connectionMock); + this.connectionMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), EasyMock.eq(newSubject)); EasyMock.expectLastCall(); - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); @@ -421,15 +440,17 @@ public class ChatRoomIrcImplTest throws OperationFailedException { final String newSubject = "My test subject!"; - this.stackMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), + this.connectionMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), EasyMock.eq(newSubject)); EasyMock.expectLastCall().andThrow(new RuntimeException("Some error")); EasyMock.expect(this.providerMock.getIrcStack()).andReturn( this.stackMock); - this.stackMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), + EasyMock.expect(this.stackMock.getConnection()).andReturn( + this.connectionMock); + this.connectionMock.setSubject(EasyMock.anyObject(ChatRoomIrcImpl.class), EasyMock.eq(newSubject)); EasyMock.expectLastCall(); - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl room = new ChatRoomIrcImpl("#my-cool-channel", this.providerMock); @@ -452,7 +473,7 @@ public class ChatRoomIrcImplTest // @Test public void testChatRoomWithAlternativePrefix() { - EasyMock.replay(this.providerMock, this.stackMock); + EasyMock.replay(this.providerMock, this.stackMock, this.connectionMock); ChatRoomIrcImpl alternative = new ChatRoomIrcImpl("&MyAlternative-channel-prefix", this.providerMock); @@ -465,11 +486,13 @@ public class ChatRoomIrcImplTest ProtocolProviderServiceIrcImpl specialProviderMock = EasyMock.createMock(ProtocolProviderServiceIrcImpl.class); IrcStack specialStackMock = EasyMock.createMock(IrcStack.class); + IrcConnection specialConnectionMock = EasyMock.createMock(IrcConnection.class); EasyMock.expect(specialProviderMock.getIrcStack()).andReturn( specialStackMock); - EasyMock.expect(specialStackMock.getChannelTypes()).andReturn( + EasyMock.expect(specialStackMock.getConnection()).andReturn(specialConnectionMock); + EasyMock.expect(specialConnectionMock.getChannelTypes()).andReturn( Sets.newHashSet('&')); - EasyMock.replay(specialProviderMock, specialStackMock); + EasyMock.replay(specialProviderMock, specialStackMock, specialConnectionMock); ChatRoomIrcImpl alternative = new ChatRoomIrcImpl("channel-name-without-prefix", specialProviderMock); |