diff options
author | Boris Grozev <boris@jitsi.org> | 2015-03-20 12:28:10 -0700 |
---|---|---|
committer | Boris Grozev <boris@jitsi.org> | 2015-03-20 12:28:10 -0700 |
commit | 100b6b88765c3aaddcb2f63785133bccb2b96423 (patch) | |
tree | 1452ae24ca3d608ba3cb27d1376e6712a895dd61 /src/net/java/sip/communicator/impl | |
parent | ec99032d9093443d1fb461125d510ef3d47d0af8 (diff) | |
parent | dcc1026578286af31ba883cec09f6383220e8a77 (diff) | |
download | jitsi-100b6b88765c3aaddcb2f63785133bccb2b96423.zip jitsi-100b6b88765c3aaddcb2f63785133bccb2b96423.tar.gz jitsi-100b6b88765c3aaddcb2f63785133bccb2b96423.tar.bz2 |
Merge branch 'merge-me-after-the-bugfix-release'
Conflicts:
lib/installer-exclude/libjitsi.jar
Diffstat (limited to 'src/net/java/sip/communicator/impl')
12 files changed, 2077 insertions, 507 deletions
diff --git a/src/net/java/sip/communicator/impl/protocol/irc/BasicPollerPresenceWatcher.java b/src/net/java/sip/communicator/impl/protocol/irc/BasicPollerPresenceWatcher.java new file mode 100644 index 0000000..cd3c00a --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/irc/BasicPollerPresenceWatcher.java @@ -0,0 +1,564 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.irc; + +import java.util.*; +import java.util.concurrent.atomic.*; + +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.state.*; + +class BasicPollerPresenceWatcher + implements PresenceWatcher +{ + /** + * Logger. + */ + private static final Logger LOGGER = Logger + .getLogger(BasicPollerPresenceWatcher.class); + + /** + * Delay before starting the presence watcher task for the first time. + */ + private static final long INITIAL_PRESENCE_WATCHER_DELAY = 10000L; + + /** + * Period for the presence watcher timer. + */ + private static final long PRESENCE_WATCHER_PERIOD = 60000L; + + /** + * Instance of IRCAPi. + */ + private final IRCApi irc; + + /** + * Connection state instance. + */ + private final IIRCState connectionState; + + /** + * The persistent presence operation set used to issue presence updates. + */ + private final OperationSetPersistentPresenceIrcImpl operationSet; + + /** + * Synchronized set of nicks to watch for presence changes. + */ + private final Set<String> nickWatchList; + + /** + * Constructor. + * + * @param irc the IRCApi instance + * @param connectionState the connection state + * @param operationSet the persistent presence operation set + * @param nickWatchList SYNCHRONIZED the nick watch list + * @param serverIdentity the server identity + */ + BasicPollerPresenceWatcher(final IRCApi irc, + final IIRCState connectionState, + final OperationSetPersistentPresenceIrcImpl operationSet, + final Set<String> nickWatchList, + final AtomicReference<String> serverIdentity) + { + if (irc == null) + { + throw new IllegalArgumentException("irc cannot be null"); + } + this.irc = irc; + if (connectionState == null) + { + throw new IllegalArgumentException("connectionState cannot be null"); + } + this.connectionState = connectionState; + if (operationSet == null) + { + throw new IllegalArgumentException("operationSet cannot be null"); + } + this.operationSet = operationSet; + if (nickWatchList == null) + { + throw new IllegalArgumentException("nickWatchList cannot be null"); + } + this.nickWatchList = nickWatchList; + setUpPresenceWatcher(serverIdentity); + LOGGER.debug("Basic Poller presence watcher initialized."); + } + + /** + * Set up a timer for watching the presence of nicks in the watch list. + */ + private void setUpPresenceWatcher( + final AtomicReference<String> serverIdentity) + { + // FIFO query list to be shared between presence watcher task and + // presence reply listener. + final List<List<String>> queryList = + Collections.synchronizedList(new LinkedList<List<String>>()); + final Timer presenceWatcher = new Timer(); + irc.addListener(new PresenceReplyListener(presenceWatcher, queryList)); + final PresenceWatcherTask task = + new PresenceWatcherTask(this.nickWatchList, queryList, + serverIdentity); + presenceWatcher.schedule(task, INITIAL_PRESENCE_WATCHER_DELAY, + PRESENCE_WATCHER_PERIOD); + LOGGER.trace("Basic Poller presence watcher set up."); + } + + @Override + public void add(String nick) + { + this.nickWatchList.add(nick); + } + + @Override + public void remove(String nick) + { + this.nickWatchList.remove(nick); + } + + /** + * Task for watching nick presence. + * + * @author Danny van Heumen + */ + private final class PresenceWatcherTask extends TimerTask + { + /** + * Static overhead for ISON response message. + * + * Additional 10 chars extra overhead as fail-safe, as I was not able to + * find the exact number in the overhead computation. + */ + private static final int ISON_RESPONSE_STATIC_MESSAGE_OVERHEAD = 18; + + /** + * Set containing nicks that must be watched. + */ + private final Set<String> watchList; + + /** + * FIFO list storing each ISON query that is sent, for use when + * responses return. + */ + private final List<List<String>> queryList; + + /** + * Reference to the current server identity. + */ + private final AtomicReference<String> serverIdentity; + + /** + * Constructor. + * + * @param watchList the list of nicks to watch + * @param queryList list containing list of nicks of each ISON query + * @param serverIdentity container with the current server identity for + * use in overhead calculation + */ + public PresenceWatcherTask(final Set<String> watchList, + final List<List<String>> queryList, + final AtomicReference<String> serverIdentity) + { + if (watchList == null) + { + throw new IllegalArgumentException("watchList cannot be null"); + } + this.watchList = watchList; + if (queryList == null) + { + throw new IllegalArgumentException("queryList cannot be null"); + } + this.queryList = queryList; + if (serverIdentity == null) + { + throw new IllegalArgumentException( + "serverIdentity reference cannot be null"); + } + this.serverIdentity = serverIdentity; + } + + /** + * The implementation of the task. + */ + @Override + public void run() + { + if (this.watchList.isEmpty()) + { + LOGGER.trace("Watch list is empty. Not querying for online " + + "presence."); + return; + } + if (this.serverIdentity.get() == null) + { + LOGGER.trace("Server identity not available yet. Skipping " + + "this presence status query."); + return; + } + LOGGER + .trace("Watch list contains nicks: querying presence status."); + final StringBuilder query = new StringBuilder(); + final LinkedList<String> list; + synchronized (this.watchList) + { + list = new LinkedList<String>(this.watchList); + } + LinkedList<String> nicks = new LinkedList<String>(); + // The ISON reply contains the most overhead, so base the maximum + // number of nicks limit on that. + final int maxQueryLength = + MessageManager.IRC_PROTOCOL_MAXIMUM_MESSAGE_SIZE - overhead(); + for (String nick : list) + { + if (query.length() + nick.length() >= maxQueryLength) + { + this.queryList.add(nicks); + BasicPollerPresenceWatcher.this.irc + .rawMessage(createQuery(query)); + // Initialize new data types + query.delete(0, query.length()); + nicks = new LinkedList<String>(); + } + query.append(nick); + query.append(' '); + nicks.add(nick); + } + if (query.length() > 0) + { + // Send remaining entries. + this.queryList.add(nicks); + BasicPollerPresenceWatcher.this.irc + .rawMessage(createQuery(query)); + } + } + + /** + * Create an ISON query from the StringBuilder containing the list of + * nicks. + * + * @param nicklist the list of nicks as a StringBuilder instance + * @return returns the ISON query string + */ + private String createQuery(final StringBuilder nicklist) + { + return "ISON " + nicklist; + } + + /** + * Calculate overhead for ISON response message. + * + * @return returns amount of overhead in response message + */ + private int overhead() + { + return ISON_RESPONSE_STATIC_MESSAGE_OVERHEAD + + this.serverIdentity.get().length() + + BasicPollerPresenceWatcher.this.connectionState.getNickname() + .length(); + } + } + + /** + * Presence reply listener. + * + * Listener that acts on various replies that give an indication of actual + * presence or presence changes, such as RPL_ISON and ERR_NOSUCHNICKCHAN. + * + * @author Danny van Heumen + */ + private final class PresenceReplyListener + extends AbstractIrcMessageListener + { + /** + * Reply for ISON query. + */ + private static final int RPL_ISON = 303; + + /** + * Error reply in case nick does not exist on server. + */ + private static final int ERR_NOSUCHNICK = 401; + + /** + * Timer for presence watcher task. + */ + private final Timer timer; + + /** + * FIFO list containing list of nicks for each query. + */ + private final List<List<String>> queryList; + + /** + * Constructor. + * + * @param timer Timer for presence watcher task + * @param queryList List of executed queries with expected nicks lists. + */ + public PresenceReplyListener(final Timer timer, + final List<List<String>> queryList) + { + super(BasicPollerPresenceWatcher.this.irc, + BasicPollerPresenceWatcher.this.connectionState); + if (timer == null) + { + throw new IllegalArgumentException("timer cannot be null"); + } + this.timer = timer; + if (queryList == null) + { + throw new IllegalArgumentException("queryList cannot be null"); + } + this.queryList = queryList; + } + + /** + * Update nick watch list upon receiving a nick change message for a + * nick that is on the watch list. + * + * NOTE: This nick change event could be handled earlier than the + * handler that fires the contact rename event. This will result in a + * missed presence update. However, since the nick change was just + * announced, it is reasonable to assume that the user is still online. + * + * @param msg the nick message + */ + @Override + public void onNickChange(final NickMessage msg) + { + if (msg == null || msg.getSource() == null) + { + return; + } + final String oldNick = msg.getSource().getNick(); + final String newNick = msg.getNewNick(); + if (oldNick == null || newNick == null) + { + LOGGER.error("Incomplete nick change message. Old nick: '" + + oldNick + "', new nick: '" + newNick + "'."); + return; + } + synchronized (BasicPollerPresenceWatcher.this.nickWatchList) + { + if (BasicPollerPresenceWatcher.this.nickWatchList + .contains(oldNick)) + { + update(oldNick, IrcStatusEnum.OFFLINE); + } + if (BasicPollerPresenceWatcher.this.nickWatchList + .contains(newNick)) + { + update(newNick, IrcStatusEnum.ONLINE); + } + } + } + + /** + * Message handling for RPL_ISON message and other indicators. + * + * @param msg the message + */ + @Override + public void onServerNumericMessage(final ServerNumericMessage msg) + { + if (msg == null || msg.getNumericCode() == null) + { + return; + } + switch (msg.getNumericCode()) + { + case RPL_ISON: + final String[] nicks = msg.getText().substring(1).split(" "); + final List<String> offline; + if (this.queryList.isEmpty()) + { + // If no query list exists, we can only update nicks that + // are online, since we do not know who we have actually + // queried for. + offline = new LinkedList<String>(); + } + else + { + offline = this.queryList.remove(0); + } + for (String nick : nicks) + { + update(nick, IrcStatusEnum.ONLINE); + offline.remove(nick); + } + for (String nick : offline) + { + update(nick, IrcStatusEnum.OFFLINE); + } + break; + case ERR_NOSUCHNICK: + final String errortext = msg.getText(); + final int idx = errortext.indexOf(' '); + if (idx == -1) + { + LOGGER.info("ERR_NOSUCHNICK message does not have " + + "expected format."); + return; + } + final String errNick = errortext.substring(0, idx); + update(errNick, IrcStatusEnum.OFFLINE); + break; + default: + break; + } + } + + /** + * Upon receiving a private message from a user, conclude that the user + * must then be online and update its presence status. + * + * @param msg the message + */ + @Override + public void onUserPrivMessage(final UserPrivMsg msg) + { + if (msg == null || msg.getSource() == null) + { + return; + } + final IRCUser user = msg.getSource(); + update(user.getNick(), IrcStatusEnum.ONLINE); + } + + /** + * Upon receiving a notice from a user, conclude that the user + * must then be online and update its presence status. + * + * @param msg the message + */ + @Override + public void onUserNotice(final UserNotice msg) + { + if (msg == null || msg.getSource() == null) + { + return; + } + final IRCUser user = msg.getSource(); + update(user.getNick(), IrcStatusEnum.ONLINE); + } + + /** + * Upon receiving an action from a user, conclude that the user + * must then be online and update its presence status. + * + * @param msg the message + */ + @Override + public void onUserAction(final UserActionMsg msg) + { + if (msg == null || msg.getSource() == null) + { + return; + } + final IRCUser user = msg.getSource(); + update(user.getNick(), IrcStatusEnum.ONLINE); + } + + /** + * Handler for channel join events. + */ + @Override + public void onChannelJoin(final ChanJoinMessage msg) + { + if (msg == null || msg.getSource() == null) + { + return; + } + final String user = msg.getSource().getNick(); + update(user, IrcStatusEnum.ONLINE); + } + + /** + * Handler for user quit events. + * + * @param msg the quit message + */ + @Override + public void onUserQuit(final QuitMessage msg) + { + super.onUserQuit(msg); + final String user = msg.getSource().getNick(); + if (user == null) + { + return; + } + if (localUser(user)) + { + // Stop presence watcher task. + this.timer.cancel(); + updateAll(IrcStatusEnum.OFFLINE); + } + else + { + update(user, IrcStatusEnum.OFFLINE); + } + } + + /** + * In case a fatal error occurs, remove the listener. + * + * @param msg the error message + */ + @Override + public void onError(final ErrorMessage msg) + { + super.onError(msg); + // Stop presence watcher task. + this.timer.cancel(); + updateAll(IrcStatusEnum.OFFLINE); + } + + /** + * Update the status of a single nick. + * + * @param nick the nick to update + * @param status the new status + */ + private void update(final String nick, final IrcStatusEnum status) + { + // User is some other user, so check if we are watching that nick. + if (!BasicPollerPresenceWatcher.this.nickWatchList.contains(nick)) + { + return; + } + BasicPollerPresenceWatcher.this.operationSet + .updateNickContactPresence(nick, status); + } + + /** + * Update the status of all contacts in the nick watch list. + * + * @param status the new status + */ + private void updateAll(final IrcStatusEnum status) + { + final LinkedList<String> list; + synchronized (BasicPollerPresenceWatcher.this.nickWatchList) + { + list = + new LinkedList<String>( + BasicPollerPresenceWatcher.this.nickWatchList); + } + for (String nick : list) + { + BasicPollerPresenceWatcher.this.operationSet + .updateNickContactPresence(nick, status); + } + } + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/irc/ContactGroupIrcImpl.java b/src/net/java/sip/communicator/impl/protocol/irc/ContactGroupIrcImpl.java index 09a997b..797ddd7 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/ContactGroupIrcImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/ContactGroupIrcImpl.java @@ -31,13 +31,13 @@ public class ContactGroupIrcImpl /** * Subgroups. */ - private final List<ContactGroupIrcImpl> subgroups = + private final ArrayList<ContactGroupIrcImpl> subgroups = new ArrayList<ContactGroupIrcImpl>(); /** * Contacts in this group. */ - private final List<ContactIrcImpl> contacts = + private final ArrayList<ContactIrcImpl> contacts = new ArrayList<ContactIrcImpl>(); /** @@ -46,11 +46,16 @@ public class ContactGroupIrcImpl private ContactGroup parent; /** + * Flag for persistence. + */ + private boolean persistent; + + /** * Contact Group IRC implementation. * * @param provider IRC protocol provider service instance. */ - public ContactGroupIrcImpl(final ProtocolProviderServiceIrcImpl provider) + ContactGroupIrcImpl(final ProtocolProviderServiceIrcImpl provider) { this(provider, null, "root"); } @@ -76,6 +81,7 @@ public class ContactGroupIrcImpl throw new IllegalArgumentException("name cannot be null"); } this.name = name; + this.persistent = true; } /** @@ -166,7 +172,7 @@ public class ContactGroupIrcImpl @Override public ContactIrcImpl getContact(final String id) { - if (id == null) + if (id == null || id.isEmpty()) { return null; } @@ -181,6 +187,34 @@ public class ContactGroupIrcImpl } /** + * Find contact by searching through direct contacts and subsequently + * continue searching in subgroups. + * + * @param id the contact id + * @return returns found contact instance or <tt>null</tt> if contact is not + * found + */ + public ContactIrcImpl findContact(final String id) + { + // search own contacts + ContactIrcImpl contact = getContact(id); + if (contact != null) + { + return contact; + } + // search in subgroups + for (ContactGroupIrcImpl subgroup : this.subgroups) + { + contact = subgroup.findContact(id); + if (contact != null) + { + return contact; + } + } + return null; + } + + /** * Check if group can contain subgroups. * * @return returns true if group can contain subgroups, or false otherwise. @@ -246,7 +280,18 @@ public class ContactGroupIrcImpl @Override public boolean isPersistent() { - return false; + return this.persistent; + } + + /** + * Set persistence. + * + * @param persistent <tt>true</tt> for persistent group, <tt>false</tt> for + * non-persistent group + */ + public void setPersistent(final boolean persistent) + { + this.persistent = persistent; } /** @@ -268,7 +313,7 @@ public class ContactGroupIrcImpl @Override public boolean isResolved() { - return false; + return true; } /** @@ -297,6 +342,20 @@ public class ContactGroupIrcImpl } /** + * Remove contact. + * + * @param contact the contact to remove + */ + public void removeContact(final ContactIrcImpl contact) + { + if (contact == null) + { + throw new IllegalArgumentException("contact cannot be null"); + } + this.contacts.remove(contact); + } + + /** * Add group as subgroup to this group. * * @param group the group @@ -309,4 +368,18 @@ public class ContactGroupIrcImpl } this.subgroups.add(group); } + + /** + * Remove subgroup from this group. + * + * @param group the group + */ + public void removeSubGroup(final ContactGroupIrcImpl group) + { + if (group == null) + { + throw new IllegalArgumentException("group cannot be null"); + } + this.subgroups.remove(group); + } } diff --git a/src/net/java/sip/communicator/impl/protocol/irc/ContactIrcImpl.java b/src/net/java/sip/communicator/impl/protocol/irc/ContactIrcImpl.java index 08ba805..ab97ee2 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/ContactIrcImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/ContactIrcImpl.java @@ -42,9 +42,11 @@ public class ContactIrcImpl * @param provider Protocol provider service instance. * @param id Contact id. * @param parentGroup The parent group of the contact. + * @param presence the initial presence status of the new contact */ public ContactIrcImpl(final ProtocolProviderServiceIrcImpl provider, - final String id, final ContactGroupIrcImpl parentGroup) + final String id, final ContactGroupIrcImpl parentGroup, + final IrcStatusEnum presence) { if (provider == null) { @@ -61,7 +63,11 @@ public class ContactIrcImpl throw new IllegalArgumentException("parentGroup cannot be null"); } this.parentGroup = parentGroup; - this.presence = IrcStatusEnum.ONLINE; + if (presence == null) + { + throw new IllegalArgumentException("presence cannot be null"); + } + this.presence = presence; } /** @@ -151,6 +157,20 @@ public class ContactIrcImpl } /** + * Set a new parent group. + * + * @param group the new parent group + */ + public void setParentContactGroup(final ContactGroupIrcImpl group) + { + if (group == null) + { + throw new IllegalArgumentException("group cannot be null"); + } + this.parentGroup = group; + } + + /** * Get protocol provider service. * * @return returns IRC protocol provider service. @@ -163,15 +183,18 @@ public class ContactIrcImpl /** * Is persistent contact. + * + * Determine persistence by the group in which it is stored. As long as a + * contact is stored in the non-persistent contact group, we will consider + * it non-persistent. As soon as a contact is moved to another group, we + * assume it is valuable, so we consider it persistent. * * @return Returns true if contact is persistent, or false otherwise. */ @Override public boolean isPersistent() { - // TODO implement notion of persistence based on whether or not the nick - // name is registered on the IRC network, for NickServ. - return false; + return this.parentGroup.isPersistent(); } /** @@ -186,7 +209,7 @@ public class ContactIrcImpl // is registered and the nick is currently "active" according to the // server, i.e. NickServ. // For now, we consider the contact unresolved ... - return false; + return true; } /** diff --git a/src/net/java/sip/communicator/impl/protocol/irc/ISupport.java b/src/net/java/sip/communicator/impl/protocol/irc/ISupport.java index 7e91de4..66e9895 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/ISupport.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/ISupport.java @@ -39,7 +39,15 @@ public enum ISupport /** * Maximum number of joined channels allowed by IRC server. */ - CHANLIMIT; + CHANLIMIT, + /** + * Maximum number of entries in the MONITOR list supported by this server. + */ + MONITOR, + /** + * Maximum number of entries in the WATCH list supported by this server. + */ + WATCH; /** * Pattern for parsing ChanLimit ISUPPORT parameter. 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 a6ac010..f5a9588 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java @@ -221,6 +221,16 @@ public class IrcStack implements IrcConnectionListener } /** + * Get the stack's persistent context instance. + * + * @return returns this stack's persistent context instance + */ + PersistentContext getContext() + { + return this.context; + } + + /** * Create a custom SSL context for this particular server. * * @param hostname host name of the host we are connecting to such that we diff --git a/src/net/java/sip/communicator/impl/protocol/irc/MonitorPresenceWatcher.java b/src/net/java/sip/communicator/impl/protocol/irc/MonitorPresenceWatcher.java new file mode 100644 index 0000000..5cd4813 --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/irc/MonitorPresenceWatcher.java @@ -0,0 +1,345 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.irc; + +import java.util.*; + +import net.java.sip.communicator.util.*; + +import com.ircclouds.irc.api.*; +import com.ircclouds.irc.api.domain.messages.*; +import com.ircclouds.irc.api.state.*; + +/** + * MONITOR presence watcher. + * + * @author Danny van Heumen + */ +class MonitorPresenceWatcher + implements PresenceWatcher +{ + /** + * Static overhead in message payload required for 'MONITOR +' command. + */ + private static final int MONITOR_ADD_CMD_STATIC_OVERHEAD = 10; + + /** + * Logger. + */ + private static final Logger LOGGER = Logger + .getLogger(MonitorPresenceWatcher.class); + + /** + * IRCApi instance. + */ + private final IRCApi irc; + + /** + * IRC connection state. + */ + private final IIRCState connectionState; + + /** + * Complete nick watch list. + */ + private final Set<String> nickWatchList; + + /** + * List of monitored nicks. + * + * The instance is stored here only for access in order to remove a nick + * from the list, since 'MONITOR - ' command does not reply so we cannot + * manage list removals from the reply listener. + */ + private final Set<String> monitoredList; + + /** + * Constructor. + * + * @param irc the IRCApi instance + * @param connectionState the connection state + * @param nickWatchList SYNCHRONIZED the nick watch list + * @param monitored SYNCHRONIZED The shared collection which contains all + * the nicks that are confirmed to be subscribed to the MONITOR + * command. + * @param operationSet the persistent presence operation set + */ + MonitorPresenceWatcher(final IRCApi irc, final IIRCState connectionState, + final Set<String> nickWatchList, final Set<String> monitored, + final OperationSetPersistentPresenceIrcImpl operationSet, + final int maxListSize) + { + if (irc == null) + { + throw new IllegalArgumentException("irc cannot be null"); + } + this.irc = irc; + if (connectionState == null) + { + throw new IllegalArgumentException( + "connectionState cannot be null"); + } + this.connectionState = connectionState; + if (nickWatchList == null) + { + throw new IllegalArgumentException("nickWatchList cannot be null"); + } + this.nickWatchList = nickWatchList; + if (monitored == null) + { + throw new IllegalArgumentException("monitored cannot be null"); + } + this.monitoredList = monitored; + this.irc.addListener(new MonitorReplyListener(this.monitoredList, + operationSet)); + setUpMonitor(this.irc, this.nickWatchList, maxListSize); + LOGGER.debug("MONITOR presence watcher initialized."); + } + + /** + * Set up monitor based on the nick watch list in its current state. + * + * Created a static method as not to interfere too much with a state that is + * still being initialized. + */ + private static void setUpMonitor(final IRCApi irc, + final Collection<String> nickWatchList, final int maxListSize) + { + List<String> current; + synchronized (nickWatchList) + { + current = new LinkedList<String>(nickWatchList); + } + if (current.size() > maxListSize) + { + // cut off list to maximum number of entries allowed by server + current = current.subList(0, maxListSize); + } + final int maxLength = 510 - MONITOR_ADD_CMD_STATIC_OVERHEAD; + final StringBuilder query = new StringBuilder(); + for (String nick : current) + { + if (query.length() + nick.length() + 1 > maxLength) + { + // full payload, send monitor query now + irc.rawMessage("MONITOR + " + query); + query.delete(0, query.length()); + } + else if (query.length() > 0) + { + query.append(","); + } + query.append(nick); + } + if (query.length() > 0) + { + // send query for remaining nicks + irc.rawMessage("MONITOR + " + query); + } + } + + @Override + public void add(final String nick) + { + LOGGER.trace("Adding nick '" + nick + "' to MONITOR watch list."); + this.nickWatchList.add(nick); + this.irc.rawMessage("MONITOR + " + nick); + } + + @Override + public void remove(final String nick) + { + LOGGER.trace("Removing nick '" + nick + "' from MONITOR watch list."); + this.nickWatchList.remove(nick); + this.irc.rawMessage("MONITOR - " + nick); + // 'MONITOR - nick' command does not send confirmation, so immediately + // remove nick from monitored list. + this.monitoredList.remove(nick); + } + + /** + * Listener for MONITOR replies. + * + * @author Danny van Heumen + */ + private final class MonitorReplyListener + extends AbstractIrcMessageListener + { + /** + * Numeric message id for ONLINE nick response. + */ + private static final int IRC_RPL_MONONLINE = 730; + + /** + * Numeric message id for OFFLINE nick response. + */ + private static final int IRC_RPL_MONOFFLINE = 731; + + // /** + // * Numeric message id for MONLIST entry. + // */ + // private static final int IRC_RPL_MONLIST = 732; + // + // /** + // * Numeric message id for ENDOFMONLIST. + // */ + // private static final int IRC_RPL_ENDOFMONLIST = 733; + + /** + * Error message signaling full list. Nick list provided are all nicks + * that failed to be added to the monitor list. + */ + private static final int IRC_ERR_MONLISTFULL = 734; + + /** + * Operation set persistent presence instance. + */ + private final OperationSetPersistentPresenceIrcImpl operationSet; + + /** + * Set of nicks that are confirmed to be monitored by the server. + */ + private final Set<String> monitoredNickList; + + // TODO Update to act on onClientError once available. + + /** + * Constructor. + * + * @param monitored SYNCHRONIZED Collection of monitored nicks. This + * collection will be updated with all nicks that are + * confirmed to be subscribed by the MONITOR command. + * @param operationSet the persistent presence opset used to update nick + * presence statuses. + */ + public MonitorReplyListener(final Set<String> monitored, + final OperationSetPersistentPresenceIrcImpl operationSet) + { + super(MonitorPresenceWatcher.this.irc, + MonitorPresenceWatcher.this.connectionState); + if (operationSet == null) + { + throw new IllegalArgumentException( + "operationSet cannot be null"); + } + this.operationSet = operationSet; + if (monitored == null) + { + throw new IllegalArgumentException("monitored cannot be null"); + } + this.monitoredNickList = monitored; + } + + /** + * Numeric messages received in response to MONITOR commands or presence + * updates. + */ + @Override + public void onServerNumericMessage(final ServerNumericMessage msg) + { + final List<String> acknowledged; + switch (msg.getNumericCode()) + { + case IRC_RPL_MONONLINE: + acknowledged = parseMonitorResponse(msg.getText()); + for (String nick : acknowledged) + { + update(nick, IrcStatusEnum.ONLINE); + } + monitoredNickList.addAll(acknowledged); + break; + case IRC_RPL_MONOFFLINE: + acknowledged = parseMonitorResponse(msg.getText()); + for (String nick : acknowledged) + { + update(nick, IrcStatusEnum.OFFLINE); + } + monitoredNickList.addAll(acknowledged); + break; + case IRC_ERR_MONLISTFULL: + LOGGER.debug("MONITOR list full. Nick was not added. " + + "Fall back Basic Poller will be used if it is enabled. (" + + msg.getText() + ")"); + break; + } + } + + /** + * Update all monitored nicks upon receiving a server-side QUIT message + * for local user. + */ + @Override + public void onUserQuit(QuitMessage msg) + { + super.onUserQuit(msg); + if (localUser(msg.getSource().getNick())) { + updateAll(IrcStatusEnum.OFFLINE); + } + } + + /** + * Update all monitored nicks upon receiving a server-side ERROR + * response. + */ + @Override + public void onError(ErrorMessage msg) + { + super.onError(msg); + updateAll(IrcStatusEnum.OFFLINE); + } + + /** + * Parse response messages. + * + * @param message the message + * @return Returns the list of targets extracted. + */ + private List<String> parseMonitorResponse(final String message) + { + // Note: this should support both targets consisting of only a nick, + // and targets consisting of nick!ident@host formats. (And probably + // any variation on this that is typically allowed in IRC.) + final LinkedList<String> acknowledged = new LinkedList<String>(); + final String[] targets = message.substring(1).split(","); + for (String target : targets) + { + String[] parts = target.trim().split("!"); + acknowledged.add(parts[0]); + } + return acknowledged; + } + + /** + * Update all monitored nicks to specified status. + * + * @param status the desired status + */ + private void updateAll(final IrcStatusEnum status) + { + final LinkedList<String> nicks; + synchronized (monitoredNickList) + { + nicks = new LinkedList<String>(monitoredNickList); + } + for (String nick : nicks) + { + update(nick, status); + } + } + + /** + * Update specified nick to specified presence status. + * + * @param nick the target nick + * @param status the current status + */ + private void update(final String nick, final IrcStatusEnum status) + { + this.operationSet.updateNickContactPresence(nick, status); + } + } +} 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 0fa90af..624df4b 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/OperationSetPersistentPresenceIrcImpl.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/OperationSetPersistentPresenceIrcImpl.java @@ -6,6 +6,7 @@ */ package net.java.sip.communicator.impl.protocol.irc; +import java.io.*; import java.util.*; import net.java.sip.communicator.service.protocol.*; @@ -57,7 +58,8 @@ public class OperationSetPersistentPresenceIrcImpl // Create volatile contact ContactIrcImpl newVolatileContact = - new ContactIrcImpl(this.parentProvider, id, volatileGroup); + new ContactIrcImpl(this.parentProvider, id, volatileGroup, + IrcStatusEnum.ONLINE); volatileGroup.addContact(newVolatileContact); // Add nick to watch list of presence manager. @@ -65,6 +67,8 @@ public class OperationSetPersistentPresenceIrcImpl this.parentProvider.getIrcStack().getConnection(); if (connection != null) { + // FIXME create private method that decides between adding to + // persistent context directly or adding via presence manager connection.getPresenceManager().addNickWatch(id); } @@ -99,6 +103,7 @@ public class OperationSetPersistentPresenceIrcImpl ContactGroupIrcImpl volatileGroup = new ContactGroupIrcImpl(this.parentProvider, this.rootGroup, groupName); + volatileGroup.setPersistent(false); this.rootGroup.addSubGroup(volatileGroup); @@ -132,18 +137,7 @@ public class OperationSetPersistentPresenceIrcImpl IllegalStateException, OperationFailedException { - if (contactIdentifier == null) - { - throw new IllegalArgumentException( - "contactIdentifier cannot be null"); - } - final IrcConnection connection = - this.parentProvider.getIrcStack().getConnection(); - if (connection == null) - { - return; - } - connection.getPresenceManager().addNickWatch(contactIdentifier); + subscribe(this.rootGroup, contactIdentifier); } /** @@ -162,18 +156,44 @@ public class OperationSetPersistentPresenceIrcImpl IllegalStateException, OperationFailedException { - if (contactIdentifier == null) + if (contactIdentifier == null || contactIdentifier.isEmpty()) + { + throw new IllegalArgumentException( + "contactIdentifier cannot be null or empty"); + } + if (!(parent instanceof ContactGroupIrcImpl)) { throw new IllegalArgumentException( - "contactIdentifier cannot be null"); + "parent group must be an instance of ContactGroupIrcImpl"); } + final ContactGroupIrcImpl contactGroup = (ContactGroupIrcImpl) parent; final IrcConnection connection = this.parentProvider.getIrcStack().getConnection(); if (connection == null) { - return; + throw new IllegalStateException("not currently connected"); + } + // TODO show some kind of confirmation dialog before adding a contact, + // since contacts in IRC are not always authenticated. + // TODO verify id with IdentityService (future) to ensure that user is + // authenticated before adding it (ACC 3: user is logged in, ACC 0: user + // does not exist, ACC 1: account exists but user is not logged in) + final ContactIrcImpl newContact = + new ContactIrcImpl(this.parentProvider, contactIdentifier, + contactGroup, IrcStatusEnum.OFFLINE); + try + { + contactGroup.addContact(newContact); + connection.getPresenceManager().addNickWatch(contactIdentifier); + fireSubscriptionEvent(newContact, contactGroup, + SubscriptionEvent.SUBSCRIPTION_CREATED); + } + catch (RuntimeException e) + { + LOGGER.debug("Failed to subscribe to contact.", e); + fireSubscriptionEvent(newContact, contactGroup, + SubscriptionEvent.SUBSCRIPTION_FAILED); } - connection.getPresenceManager().addNickWatch(contactIdentifier); } /** @@ -190,21 +210,38 @@ public class OperationSetPersistentPresenceIrcImpl IllegalStateException, OperationFailedException { - if (contact == null) + if (!(contact instanceof ContactIrcImpl)) + { + throw new IllegalArgumentException( + "contact must be instance of ContactIrcImpl"); + } + final ContactIrcImpl ircContact = (ContactIrcImpl) contact; + final ContactGroupIrcImpl parentGroup = + (ContactGroupIrcImpl) ircContact.getParentContactGroup(); + try { - throw new IllegalArgumentException("contact cannot be null"); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + if (connection != null) + { + connection.getPresenceManager().removeNickWatch( + contact.getAddress()); + } + parentGroup.removeContact(ircContact); + fireSubscriptionEvent(ircContact, parentGroup, + SubscriptionEvent.SUBSCRIPTION_REMOVED); } - final IrcConnection connection = - this.parentProvider.getIrcStack().getConnection(); - if (connection == null) + catch (RuntimeException e) { - return; + LOGGER.debug("Failed to unsubscribe from contact.", e); + fireSubscriptionEvent(ircContact, parentGroup, + SubscriptionEvent.SUBSCRIPTION_FAILED); } - connection.getPresenceManager().removeNickWatch(contact.getAddress()); } /** - * Creating a contact group is currently not implemented. + * Create a "server stored" contact group. (Which is not actually server + * stored, but close enough ...) * * @param parent parent contact group * @param groupName new group's name @@ -215,8 +252,22 @@ public class OperationSetPersistentPresenceIrcImpl final String groupName) throws OperationFailedException { LOGGER.trace("createServerStoredContactGroup(...) called"); - throw new OperationFailedException("Not implemented.", - OperationFailedException.NOT_SUPPORTED_OPERATION); + if (!(parent instanceof ContactGroupIrcImpl)) + { + throw new IllegalArgumentException( + "parent is not an instance of ContactGroupIrcImpl"); + } + if (groupName == null || groupName.isEmpty()) + { + throw new IllegalArgumentException( + "groupName cannot be null or empty"); + } + final ContactGroupIrcImpl parentGroup = (ContactGroupIrcImpl) parent; + final ContactGroupIrcImpl newGroup = + new ContactGroupIrcImpl(this.parentProvider, parentGroup, groupName); + parentGroup.addSubGroup(newGroup); + fireServerStoredGroupEvent(newGroup, + ServerStoredGroupEvent.GROUP_CREATED_EVENT); } /** @@ -230,8 +281,16 @@ public class OperationSetPersistentPresenceIrcImpl throws OperationFailedException { LOGGER.trace("removeServerStoredContactGroup called"); - throw new OperationFailedException("Not implemented.", - OperationFailedException.NOT_SUPPORTED_OPERATION); + if (!(group instanceof ContactGroupIrcImpl)) + { + throw new IllegalArgumentException( + "group must be an instance of ContactGroupIrcImpl"); + } + final ContactGroupIrcImpl ircGroup = (ContactGroupIrcImpl) group; + ((ContactGroupIrcImpl) ircGroup.getParentContactGroup()) + .removeSubGroup(ircGroup); + fireServerStoredGroupEvent(ircGroup, + ServerStoredGroupEvent.GROUP_REMOVED_EVENT); } /** @@ -260,8 +319,20 @@ public class OperationSetPersistentPresenceIrcImpl final ContactGroup newParent) throws OperationFailedException { LOGGER.trace("moveContactToGroup called"); - throw new OperationFailedException("Not implemented.", - OperationFailedException.NOT_SUPPORTED_OPERATION); + if (!(contactToMove instanceof ContactIrcImpl)) + { + throw new IllegalArgumentException( + "contactToMove must be an instance of ContactIrcImpl"); + } + final ContactIrcImpl contact = (ContactIrcImpl) contactToMove; + // remove contact from old parent contact group + ((ContactGroupIrcImpl) contact.getParentContactGroup()) + .removeContact(contact); + // add contact to new parent contact group + final ContactGroupIrcImpl newGroup = (ContactGroupIrcImpl) newParent; + newGroup.addContact(contact); + // update parent contact group in contact + contact.setParentContactGroup(newGroup); } /** @@ -272,9 +343,6 @@ public class OperationSetPersistentPresenceIrcImpl @Override public ContactGroup getServerStoredContactListRoot() { - // TODO consider using this for contacts that are registered at NickServ - // for the IRC network. Store contacts and possibly some whois info if - // useful for these contacts as persistent data. return this.rootGroup; } @@ -283,20 +351,13 @@ public class OperationSetPersistentPresenceIrcImpl * * @param address contact address * @param persistentData persistent data for contact - * @param parentGroup parent group to contact * @return returns newly created unresolved contact instance */ @Override public ContactIrcImpl createUnresolvedContact(final String address, - final String persistentData, final ContactGroup parentGroup) + final String persistentData) { - if (!(parentGroup instanceof ContactGroupIrcImpl)) - { - throw new IllegalArgumentException( - "Provided contact group is not an IRC contact group instance."); - } - return new ContactIrcImpl(this.parentProvider, address, - (ContactGroupIrcImpl) parentGroup); + return createUnresolvedContact(address, persistentData, this.rootGroup); } /** @@ -304,14 +365,33 @@ public class OperationSetPersistentPresenceIrcImpl * * @param address contact address * @param persistentData persistent data for contact + * @param parentGroup parent group to contact * @return returns newly created unresolved contact instance */ @Override public ContactIrcImpl createUnresolvedContact(final String address, - final String persistentData) + final String persistentData, final ContactGroup parentGroup) { - return new ContactIrcImpl(this.parentProvider, address, - this.getRootGroup()); + // FIXME actually make this thing unresolved until the first presence + // update is received? + if (address == null || address.isEmpty()) + { + throw new IllegalArgumentException( + "address cannot be null or empty"); + } + if (!(parentGroup instanceof ContactGroupIrcImpl)) + { + throw new IllegalArgumentException( + "Provided contact group is not an IRC contact group instance."); + } + final ContactGroupIrcImpl group = (ContactGroupIrcImpl) parentGroup; + final ContactIrcImpl unresolvedContact = + new ContactIrcImpl(this.parentProvider, address, + (ContactGroupIrcImpl) parentGroup, IrcStatusEnum.OFFLINE); + group.addContact(unresolvedContact); + this.parentProvider.getIrcStack().getContext().nickWatchList + .add(address); + return unresolvedContact; } /** @@ -332,8 +412,11 @@ public class OperationSetPersistentPresenceIrcImpl throw new IllegalArgumentException( "parentGroup is not a ContactGroupIrcImpl instance"); } - return new ContactGroupIrcImpl(this.parentProvider, - (ContactGroupIrcImpl) parentGroup, groupUID); + final ContactGroupIrcImpl unresolvedGroup = + new ContactGroupIrcImpl(this.parentProvider, + (ContactGroupIrcImpl) parentGroup, groupUID); + ((ContactGroupIrcImpl) parentGroup).addSubGroup(unresolvedGroup); + return unresolvedGroup; } /** @@ -436,11 +519,11 @@ public class OperationSetPersistentPresenceIrcImpl } /** - * IRC currently does not implement querying presence status. + * Query contact status using WHOIS query to IRC server. * * @param contactIdentifier contact id * @return returns current presence status - * @throws OperationFailedException for not supporting this feature + * @throws OperationFailedException in case of problems during query */ @Override public PresenceStatus queryContactStatus(final String contactIdentifier) @@ -448,9 +531,26 @@ public class OperationSetPersistentPresenceIrcImpl IllegalStateException, OperationFailedException { - // TODO implement querying presence status of contact - throw new OperationFailedException("Not supported.", - OperationFailedException.NOT_SUPPORTED_OPERATION); + final IrcConnection connection = + this.parentProvider.getIrcStack().getConnection(); + if (connection == null) + { + throw new IllegalStateException("not connected"); + } + try + { + return connection.getPresenceManager().query(contactIdentifier); + } + catch (IOException e) + { + throw new OperationFailedException("Presence query failed.", + OperationFailedException.NETWORK_FAILURE, e); + } + catch (InterruptedException e) + { + throw new OperationFailedException("Presence query interrupted.", + OperationFailedException.GENERAL_ERROR, e); + } } /** @@ -462,26 +562,7 @@ public class OperationSetPersistentPresenceIrcImpl @Override public ContactIrcImpl findContactByID(final String contactID) { - if (contactID == null) - { - return null; - } - ContactIrcImpl contact = this.rootGroup.getContact(contactID); - if (contact != null) - { - return contact; - } - Iterator<ContactGroup> groups = this.rootGroup.subgroups(); - while (groups.hasNext()) - { - ContactGroupIrcImpl group = (ContactGroupIrcImpl) groups.next(); - contact = group.getContact(contactID); - if (contact != null) - { - return contact; - } - } - return null; + return this.rootGroup.findContact(contactID); } /** diff --git a/src/net/java/sip/communicator/impl/protocol/irc/PresenceManager.java b/src/net/java/sip/communicator/impl/protocol/irc/PresenceManager.java index 1494140..57a5f0d 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/PresenceManager.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/PresenceManager.java @@ -6,29 +6,31 @@ */ package net.java.sip.communicator.impl.protocol.irc; +import java.io.*; import java.util.*; import java.util.concurrent.atomic.*; +import net.java.sip.communicator.impl.protocol.irc.collection.*; 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.state.*; /** * Manager for presence status of IRC connection. * - * There is (somewhat primitive) support for online presence by periodically - * querying IRC server with ISON requests for each of the members in the contact - * list. + * There is support for online presence by polling (periodically querying IRC + * server with ISON requests) for each of the members in the contact list or, if + * supported by the IRC server, for the MONITOR command to subscribe to presence + * notifications for the specified nick. * * TODO Support for 'a' (Away) user mode. (Check this again, since I also see * 'a' used for other purposes. This may be one of those ambiguous letters that * every server interprets differently.) * - * TODO Improve presence watcher by using WATCH or MONITOR. (Monitor does not - * seem to support away status, though) + * TODO Support away-notify extension (CAP) and handle AWAY messages + * appropriately. * * @author Danny van Heumen */ @@ -41,16 +43,6 @@ public class PresenceManager .getLogger(PresenceManager.class); /** - * Delay before starting the presence watcher task for the first time. - */ - private static final long INITIAL_PRESENCE_WATCHER_DELAY = 10000L; - - /** - * Period for the presence watcher timer. - */ - private static final long PRESENCE_WATCHER_PERIOD = 60000L; - - /** * IRC client library instance. * * Instance must be thread-safe! @@ -68,9 +60,9 @@ public class PresenceManager private final OperationSetPersistentPresenceIrcImpl operationSet; /** - * Synchronized set of nicks to watch for presence changes. + * Presence watcher. */ - private final SortedSet<String> nickWatchList; + private final PresenceWatcher watcher; /** * Maximum away message length according to server ISUPPORT instructions. @@ -80,6 +72,26 @@ public class PresenceManager private final Integer isupportAwayLen; /** + * Maximum size of MONITOR list allowed by server. + * + * <p> + * This value is not guaranteed, so it may be <tt>null</tt>. If it is + * <tt>null</tt> this means that MONITOR is not supported by this server. + * </p> + */ + private final Integer isupportMonitor; + + /** + * Maximum size of WATCH list allowed by server. + * + * <p> + * This value is not guaranteed, so it may be <tt>null</tt>. If it is + * <tt>null</tt> this means that WATCH is not supported by this server. + * </p> + */ + private final Integer isupportWatch; + + /** * Server identity. */ private final AtomicReference<String> serverIdentity = @@ -132,40 +144,90 @@ public class PresenceManager throw new IllegalArgumentException("irc cannot be null"); } this.irc = irc; + final SortedSet<String> nickWatchList; if (persistentNickWatchList == null) { - this.nickWatchList = + // watch list will be non-persistent, since we create an instance at + // initialization time + nickWatchList = Collections.synchronizedSortedSet(new TreeSet<String>()); } else { - this.nickWatchList = persistentNickWatchList; + nickWatchList = persistentNickWatchList; } - this.irc.addListener(new PresenceListener()); + this.irc.addListener(new LocalUserPresenceListener()); + // TODO move parse methods to ISupport enum type this.isupportAwayLen = parseISupportAwayLen(this.connectionState); - if (config.isContactPresenceTaskEnabled()) + this.isupportMonitor = parseISupportMonitor(this.connectionState); + this.isupportWatch = parseISupportWatch(this.connectionState); + final boolean enablePresencePolling = + config.isContactPresenceTaskEnabled(); + if (this.isupportMonitor != null) + { + // Share a list of monitored nicks between the + // MonitorPresenceWatcher and the BasicPollerPresenceWatcher. + // Now it is possible for the basic poller to determine whether + // or not to poll for a certain nick, such that we do not poll + // nicks that are already monitored. + final SortedSet<String> monitoredNicks = + Collections.synchronizedSortedSet(new TreeSet<String>()); + this.watcher = + new MonitorPresenceWatcher(this.irc, this.connectionState, + nickWatchList, monitoredNicks, this.operationSet, + this.isupportMonitor); + if (enablePresencePolling) + { + // Enable basic poller as fall back mechanism. + + // Create a dynamic set that automatically computes the + // difference between the full nick list and the list of nicks + // that are subscribed to MONITOR. The difference will be the + // result that is used by the basic poller. + final Set<String> unmonitoredNicks = + new DynamicDifferenceSet<String>(nickWatchList, + monitoredNicks); + new BasicPollerPresenceWatcher(this.irc, this.connectionState, + this.operationSet, unmonitoredNicks, this.serverIdentity); + } + } + else if (this.isupportWatch != null) { - setUpPresenceWatcher(); + // Share a list of monitored nicks between the + // WatchPresenceWatcher and the BasicPollerPresenceWatcher. + // Now it is possible for the basic poller to determine whether + // or not to poll for a certain nick, such that we do not poll + // nicks that are already monitored. + final SortedSet<String> monitoredNicks = + Collections.synchronizedSortedSet(new TreeSet<String>()); + this.watcher = + new WatchPresenceWatcher(this.irc, this.connectionState, + nickWatchList, monitoredNicks, this.operationSet, + this.isupportWatch); + if (enablePresencePolling) + { + // Enable basic poller as fall back mechanism. + + // Create a dynamic set that automatically computes the + // difference between the full nick list and the list of nicks + // that are subscribed to WATCH. The difference will be the + // result that is used by the basic poller. + final Set<String> unmonitoredNicks = + new DynamicDifferenceSet<String>(nickWatchList, + monitoredNicks); + new BasicPollerPresenceWatcher(this.irc, this.connectionState, + this.operationSet, unmonitoredNicks, this.serverIdentity); + } + } + else if (enablePresencePolling) + { + // Enable basic poller as the only presence mechanism. + this.watcher = + new BasicPollerPresenceWatcher(this.irc, this.connectionState, + this.operationSet, nickWatchList, this.serverIdentity); + } else { + this.watcher = null; } - } - - /** - * Set up a timer for watching the presence of nicks in the watch list. - */ - private void setUpPresenceWatcher() - { - // FIFO query list to be shared between presence watcher task and - // presence reply listener. - final List<List<String>> queryList = - Collections.synchronizedList(new LinkedList<List<String>>()); - final Timer presenceWatcher = new Timer(); - irc.addListener(new PresenceReplyListener(presenceWatcher, queryList)); - final PresenceWatcherTask task = - new PresenceWatcherTask(this.nickWatchList, queryList, this.irc, - this.connectionState, this.serverIdentity); - presenceWatcher.schedule(task, INITIAL_PRESENCE_WATCHER_DELAY, - PRESENCE_WATCHER_PERIOD); - LOGGER.trace("Presence watcher set up."); } /** @@ -181,6 +243,8 @@ public class PresenceManager state.getServerOptions().getKey(ISupport.AWAYLEN.name()); if (value == null) { + LOGGER.trace("No ISUPPORT parameter " + ISupport.AWAYLEN.name() + + " available."); return null; } if (LOGGER.isDebugEnabled()) @@ -188,7 +252,83 @@ public class PresenceManager LOGGER.debug("Setting ISUPPORT parameter " + ISupport.AWAYLEN.name() + " to " + value); } - return new Integer(value); + try + { + return new Integer(value); + } + catch (RuntimeException e) + { + LOGGER.warn("Failed to parse AWAYLEN value.", e); + return null; + } + } + + /** + * Parse the ISUPPORT parameter for MONITOR command support and list size. + * + * @param state the connection state + * @return Returns instance with maximum number of entries in MONITOR list. + * Additionally, having this MONITOR property available, indicates + * that MONITOR is supported by the server. + */ + private Integer parseISupportMonitor(final IIRCState state) + { + final String value = + state.getServerOptions().getKey(ISupport.MONITOR.name()); + if (value == null) + { + LOGGER.trace("No ISUPPORT parameter " + ISupport.MONITOR.name() + + " available."); + return null; + } + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Setting ISUPPORT parameter " + + ISupport.MONITOR.name() + " to " + value); + } + try + { + return new Integer(value); + } + catch (RuntimeException e) + { + LOGGER.warn("Failed to parse MONITOR value.", e); + return null; + } + } + + /** + * Parse the ISUPPORT parameter for WATCH command support and list size. + * + * @param state the connection state + * @return Returns instance with maximum number of entries in WATCH list. + * Additionally, having this WATCH property available, indicates + * that WATCH is supported by the server. + */ + private Integer parseISupportWatch(final IIRCState state) + { + final String value = + state.getServerOptions().getKey(ISupport.WATCH.name()); + if (value == null) + { + LOGGER.trace("No ISUPPORT parameter " + ISupport.WATCH.name() + + " available."); + return null; + } + if (LOGGER.isDebugEnabled()) + { + LOGGER.debug("Setting ISUPPORT parameter " + ISupport.WATCH.name() + + " to " + value); + } + try + { + return new Integer(value); + } + catch (RuntimeException e) + { + LOGGER.warn("Failed to parse WATCH value.", e); + return null; + } } /** @@ -266,13 +406,57 @@ public class PresenceManager } /** + * Query presence of provided nick. + * + * @param nick the nick + * @return returns presence status + * @throws InterruptedException interrupted exception in case waiting for + * WHOIS reply is interrupted + * @throws IOException an exception occurred during the querying process + */ + public IrcStatusEnum query(final String nick) + throws InterruptedException, + IOException + { + final Result<IrcStatusEnum, IllegalStateException> result = + new Result<IrcStatusEnum, IllegalStateException>( + IrcStatusEnum.OFFLINE); + synchronized (result) + { + this.irc.addListener(new WhoisReplyListener(nick, result)); + this.irc.rawMessage("WHOIS " + + IdentityManager.checkNick(nick, null)); + while (!result.isDone()) + { + LOGGER.debug("Waiting for presence status based on WHOIS " + + "reply ..."); + result.wait(); + } + } + final Exception exception = result.getException(); + if (exception == null) + { + return result.getValue(); + } + else + { + throw new IOException( + "An exception occured while querying whois info.", + result.getException()); + } + } + + /** * Add new nick to watch list. * * @param nick nick to add to watch list */ public void addNickWatch(final String nick) { - this.nickWatchList.add(nick); + if (this.watcher != null) + { + this.watcher.add(nick); + } } /** @@ -282,7 +466,10 @@ public class PresenceManager */ public void removeNickWatch(final String nick) { - this.nickWatchList.remove(nick); + if (this.watcher != null) + { + this.watcher.remove(nick); + } } /** @@ -291,7 +478,7 @@ public class PresenceManager * * @author Danny van Heumen */ - private final class PresenceListener + private final class LocalUserPresenceListener extends AbstractIrcMessageListener { /** @@ -308,7 +495,7 @@ public class PresenceManager /** * Constructor. */ - public PresenceListener() + public LocalUserPresenceListener() { super(PresenceManager.this.irc, PresenceManager.this.connectionState); @@ -356,307 +543,104 @@ public class PresenceManager } /** - * Task for watching nick presence. + * Listener for WHOIS replies, such that we can receive information of the + * user that we are querying. * * @author Danny van Heumen */ - private static final class PresenceWatcherTask extends TimerTask + private final class WhoisReplyListener + extends AbstractIrcMessageListener { + // TODO handle ClientError once available /** - * Static overhead for ISON response message. - * - * Additional 10 chars extra overhead as fail-safe, as I was not able to - * find the exact number in the overhead computation. + * Reply for away message. */ - private static final int ISON_RESPONSE_STATIC_MESSAGE_OVERHEAD = 18; + private static final int IRC_RPL_AWAY = 301; /** - * List containing nicks that must be watched. + * Reply for WHOIS query with user info. */ - private final SortedSet<String> watchList; + private static final int IRC_RPL_WHOISUSER = 311; /** - * FIFO list storing each ISON query that is sent, for use when - * responses return. + * Reply for signaling end of WHOIS query. */ - private final List<List<String>> queryList; + private static final int IRC_RPL_ENDOFWHOIS = 318; /** - * IRC instance. + * The nick that is being queried. */ - private final IRCApi irc; + private final String nick; /** - * IRC connection state. + * The result instance that will be updated after having received the + * RPL_ENDOFWHOIS reply. */ - private final IIRCState connectionState; + private final Result<IrcStatusEnum, IllegalStateException> result; /** - * Reference to the current server identity. + * Intermediate presence status. Updated upon receiving new WHOIS + * information. */ - private final AtomicReference<String> serverIdentity; + private IrcStatusEnum presence; /** * Constructor. * - * @param watchList the list of nicks to watch - * @param queryList list containing list of nicks of each ISON query - * @param irc the irc instance - * @param connectionState the connection state instance - * @param serverIdentity container with the current server identity for - * use in overhead calculation + * @param nick the nick + * @param result the result */ - public PresenceWatcherTask(final SortedSet<String> watchList, - final List<List<String>> queryList, final IRCApi irc, - final IIRCState connectionState, - final AtomicReference<String> serverIdentity) - { - if (watchList == null) - { - throw new IllegalArgumentException("watchList cannot be null"); - } - this.watchList = watchList; - if (queryList == null) - { - throw new IllegalArgumentException("queryList cannot be null"); - } - this.queryList = queryList; - if (irc == null) - { - throw new IllegalArgumentException("irc cannot be null"); - } - this.irc = irc; - if (connectionState == null) - { - throw new IllegalArgumentException( - "connectionState cannot be null"); - } - this.connectionState = connectionState; - if (serverIdentity == null) - { - throw new IllegalArgumentException( - "serverIdentity reference cannot be null"); - } - this.serverIdentity = serverIdentity; - } - - /** - * The implementation of the task. - */ - @Override - public void run() - { - if (this.watchList.isEmpty()) - { - LOGGER.trace("Watch list is empty. Not querying for online " - + "presence."); - return; - } - if (this.serverIdentity.get() == null) - { - LOGGER.trace("Server identity not available yet. Skipping " - + "this presence status query."); - return; - } - LOGGER - .trace("Watch list contains nicks: querying presence status."); - final StringBuilder query = new StringBuilder(); - final LinkedList<String> list; - synchronized (this.watchList) - { - list = new LinkedList<String>(this.watchList); - } - LinkedList<String> nicks = new LinkedList<String>(); - // The ISON reply contains the most overhead, so base the maximum - // number of nicks limit on that. - final int maxQueryLength = - MessageManager.IRC_PROTOCOL_MAXIMUM_MESSAGE_SIZE - overhead(); - for (String nick : list) - { - if (query.length() + nick.length() >= maxQueryLength) - { - this.queryList.add(nicks); - this.irc.rawMessage(createQuery(query)); - // Initialize new data types - query.delete(0, query.length()); - nicks = new LinkedList<String>(); - } - query.append(nick); - query.append(' '); - nicks.add(nick); - } - if (query.length() > 0) - { - // Send remaining entries. - this.queryList.add(nicks); - this.irc.rawMessage(createQuery(query)); - } - } - - /** - * Create an ISON query from the StringBuilder containing the list of - * nicks. - * - * @param nicklist the list of nicks as a StringBuilder instance - * @return returns the ISON query string - */ - private String createQuery(final StringBuilder nicklist) - { - return "ISON " + nicklist; - } - - /** - * Calculate overhead for ISON response message. - * - * @return returns amount of overhead in response message - */ - private int overhead() - { - return ISON_RESPONSE_STATIC_MESSAGE_OVERHEAD - + this.serverIdentity.get().length() - + this.connectionState.getNickname().length(); - } - } - - /** - * Presence reply listener. - * - * Listener that acts on various replies that give an indication of actual - * presence or presence changes, such as RPL_ISON and ERR_NOSUCHNICKCHAN. - * - * @author Danny van Heumen - */ - private final class PresenceReplyListener - extends AbstractIrcMessageListener - { - /** - * Reply for ISON query. - */ - private static final int RPL_ISON = 303; - - /** - * Error reply in case nick does not exist on server. - */ - private static final int ERR_NOSUCHNICK = 401; - - /** - * Timer for presence watcher task. - */ - private final Timer timer; - - /** - * FIFO list containing list of nicks for each query. - */ - private final List<List<String>> queryList; - - /** - * Constructor. - * - * @param timer Timer for presence watcher task - * @param queryList List of executed queries with expected nicks lists. - */ - public PresenceReplyListener(final Timer timer, - final List<List<String>> queryList) + private WhoisReplyListener(final String nick, + final Result<IrcStatusEnum, IllegalStateException> result) { super(PresenceManager.this.irc, PresenceManager.this.connectionState); - if (timer == null) - { - throw new IllegalArgumentException("timer cannot be null"); - } - this.timer = timer; - if (queryList == null) - { - throw new IllegalArgumentException("queryList cannot be null"); - } - this.queryList = queryList; - } - - /** - * Update nick watch list upon receiving a nick change message for a - * nick that is on the watch list. - * - * NOTE: This nick change event could be handled earlier than the - * handler that fires the contact rename event. This will result in a - * missed presence update. However, since the nick change was just - * announced, it is reasonable to assume that the user is still online. - * - * @param msg the nick message - */ - @Override - public void onNickChange(final NickMessage msg) - { - if (msg == null || msg.getSource() == null) - { - return; - } - final String oldNick = msg.getSource().getNick(); - final String newNick = msg.getNewNick(); - if (oldNick == null || newNick == null) + if (nick == null) { - LOGGER.error("Incomplete nick change message. Old nick: '" - + oldNick + "', new nick: '" + newNick + "'."); - return; + throw new IllegalArgumentException("Invalid nick specified."); } - synchronized (PresenceManager.this.nickWatchList) + this.nick = nick; + if (result == null) { - if (PresenceManager.this.nickWatchList.contains(oldNick)) - { - PresenceManager.this.nickWatchList.remove(oldNick); - PresenceManager.this.nickWatchList.add(newNick); - } + throw new IllegalArgumentException("Invalid result."); } + this.result = result; + this.presence = IrcStatusEnum.OFFLINE; } /** - * Message handling for RPL_ISON message and other indicators. + * Handle the numeric messages that the WHOIS answer consists of. * - * @param msg the message + * @param msg the numeric message */ @Override public void onServerNumericMessage(final ServerNumericMessage msg) { - if (msg == null || msg.getNumericCode() == null) + if (!this.nick.equals(msg.getTarget())) { return; } switch (msg.getNumericCode()) { - case RPL_ISON: - final String[] nicks = msg.getText().substring(1).split(" "); - final List<String> offline; - if (this.queryList.isEmpty()) - { - // If no query list exists, we can only update nicks that - // are online, since we do not know who we have actually - // queried for. - offline = new LinkedList<String>(); - } - else - { - offline = this.queryList.remove(0); - } - for (String nick : nicks) - { - update(nick, IrcStatusEnum.ONLINE); - offline.remove(nick); - } - for (String nick : offline) + case IRC_RPL_WHOISUSER: + if (this.presence != IrcStatusEnum.AWAY) { - update(nick, IrcStatusEnum.OFFLINE); + // only update presence if not set to away, since away + // status is more specific than the more general information + // of being online + this.presence = IrcStatusEnum.ONLINE; } break; - case ERR_NOSUCHNICK: - final String errortext = msg.getText(); - final int idx = errortext.indexOf(' '); - if (idx == -1) + case IRC_RPL_AWAY: + this.presence = IrcStatusEnum.AWAY; + break; + case IRC_RPL_ENDOFWHOIS: + this.irc.deleteListener(this); + synchronized (this.result) { - LOGGER.info("ERR_NOSUCHNICK message does not have " - + "expected format."); - return; + this.result.setDone(this.presence); + this.result.notifyAll(); } - final String errNick = errortext.substring(0, idx); - update(errNick, IrcStatusEnum.OFFLINE); break; default: break; @@ -664,144 +648,35 @@ public class PresenceManager } /** - * Upon receiving a private message from a user, conclude that the user - * must then be online and update its presence status. - * - * @param msg the message + * Upon connection quitting, set exception and return result. */ @Override - public void onUserPrivMessage(final UserPrivMsg msg) - { - if (msg == null || msg.getSource() == null) - { - return; - } - final IRCUser user = msg.getSource(); - update(user.getNick(), IrcStatusEnum.ONLINE); - } - - /** - * Upon receiving a notice from a user, conclude that the user - * must then be online and update its presence status. - * - * @param msg the message - */ - @Override - public void onUserNotice(final UserNotice msg) - { - if (msg == null || msg.getSource() == null) - { - return; - } - final IRCUser user = msg.getSource(); - update(user.getNick(), IrcStatusEnum.ONLINE); - } - - /** - * Upon receiving an action from a user, conclude that the user - * must then be online and update its presence status. - * - * @param msg the message - */ - @Override - public void onUserAction(final UserActionMsg msg) - { - if (msg == null || msg.getSource() == null) - { - return; - } - final IRCUser user = msg.getSource(); - update(user.getNick(), IrcStatusEnum.ONLINE); - } - - /** - * Handler for channel join events. - */ - @Override - public void onChannelJoin(final ChanJoinMessage msg) - { - if (msg == null || msg.getSource() == null) - { - return; - } - final String user = msg.getSource().getNick(); - update(user, IrcStatusEnum.ONLINE); - } - - /** - * Handler for user quit events. - * - * @param msg the quit message - */ - @Override - public void onUserQuit(final QuitMessage msg) + public void onUserQuit(QuitMessage msg) { super.onUserQuit(msg); - final String user = msg.getSource().getNick(); - if (user == null) - { - return; - } - if (localUser(user)) - { - // Stop presence watcher task. - this.timer.cancel(); - updateAll(IrcStatusEnum.OFFLINE); - } - else + if (localUser(msg.getSource().getNick())) { - update(user, IrcStatusEnum.OFFLINE); + synchronized (this.result) + { + this.result.setDone(new IllegalStateException( + "Local user quit.")); + this.result.notifyAll(); + } } } /** - * In case a fatal error occurs, remove the listener. - * - * @param msg the error message + * Upon receiving an error, set exception and return result. */ @Override - public void onError(final ErrorMessage msg) + public void onError(ErrorMessage msg) { super.onError(msg); - // Stop presence watcher task. - this.timer.cancel(); - updateAll(IrcStatusEnum.OFFLINE); - } - - /** - * Update the status of a single nick. - * - * @param nick the nick to update - * @param status the new status - */ - private void update(final String nick, final IrcStatusEnum status) - { - // User is some other user, so check if we are watching that nick. - if (!PresenceManager.this.nickWatchList.contains(nick)) - { - return; - } - PresenceManager.this.operationSet.updateNickContactPresence(nick, - status); - } - - /** - * Update the status of all contacts in the nick watch list. - * - * @param status the new status - */ - private void updateAll(final IrcStatusEnum status) - { - final LinkedList<String> list; - synchronized (PresenceManager.this.nickWatchList) - { - list = - new LinkedList<String>(PresenceManager.this.nickWatchList); - } - for (String nick : list) + synchronized (this.result) { - PresenceManager.this.operationSet.updateNickContactPresence( - nick, status); + this.result.setDone(new IllegalStateException( + "An error occurred: " + msg.getText())); + this.result.notifyAll(); } } } diff --git a/src/net/java/sip/communicator/impl/protocol/irc/PresenceWatcher.java b/src/net/java/sip/communicator/impl/protocol/irc/PresenceWatcher.java new file mode 100644 index 0000000..dfd917d --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/irc/PresenceWatcher.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * Interface of the presence watcher. + * + * This interface defines the requirements of a presence watcher. The watcher is + * used to update contact presence for a selection of nicks. + * + * @author Danny van Heumen + */ +public interface PresenceWatcher +{ + /** + * Add a nick to the list. + * + * @param nick the nick + */ + void add(String nick); + + /** + * Remove a nick from the list. + * + * @param nick the nick + */ + void remove(String nick); +} diff --git a/src/net/java/sip/communicator/impl/protocol/irc/WatchPresenceWatcher.java b/src/net/java/sip/communicator/impl/protocol/irc/WatchPresenceWatcher.java new file mode 100644 index 0000000..53ed501 --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/irc/WatchPresenceWatcher.java @@ -0,0 +1,339 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +package net.java.sip.communicator.impl.protocol.irc; + +import java.util.*; + +import net.java.sip.communicator.util.*; + +import com.ircclouds.irc.api.*; +import com.ircclouds.irc.api.domain.messages.*; +import com.ircclouds.irc.api.state.*; + +/** + * WATCH presence watcher. + * + * @author Danny van Heumen + */ +class WatchPresenceWatcher + implements PresenceWatcher +{ + /** + * Static overhead in message payload required for 'WATCH ' command. + */ + private static final int WATCH_ADD_CMD_STATIC_OVERHEAD = 6; + + /** + * Logger. + */ + private static final Logger LOGGER = Logger + .getLogger(WatchPresenceWatcher.class); + + /** + * IRCApi instance. + */ + private final IRCApi irc; + + /** + * IRC connection state. + */ + private final IIRCState connectionState; + + /** + * Complete nick watch list. + */ + private final Set<String> nickWatchList; + + /** + * Constructor. + * + * @param irc the IRCApi instance + * @param connectionState the connection state + * @param nickWatchList SYNCHRONIZED the nick watch list + * @param monitored SYNCHRONIZED The shared collection which contains all + * the nicks that are confirmed to be subscribed to the MONITOR + * command. + * @param operationSet the persistent presence operation set + */ + WatchPresenceWatcher(final IRCApi irc, final IIRCState connectionState, + final Set<String> nickWatchList, final Set<String> monitored, + final OperationSetPersistentPresenceIrcImpl operationSet, + final int maxListSize) + { + if (irc == null) + { + throw new IllegalArgumentException("irc cannot be null"); + } + this.irc = irc; + if (connectionState == null) + { + throw new IllegalArgumentException( + "connectionState cannot be null"); + } + this.connectionState = connectionState; + if (nickWatchList == null) + { + throw new IllegalArgumentException("nickWatchList cannot be null"); + } + this.nickWatchList = nickWatchList; + this.irc.addListener(new WatchReplyListener(monitored, operationSet)); + setUpWatch(this.irc, this.nickWatchList, maxListSize); + LOGGER.debug("WATCH presence watcher initialized."); + } + + /** + * Set up monitor based on the nick watch list in its current state. + * + * Created a static method as not to interfere too much with a state that is + * still being initialized. + */ + private static void setUpWatch(final IRCApi irc, + final Collection<String> nickWatchList, final int maxListSize) + { + List<String> current; + synchronized (nickWatchList) + { + current = new LinkedList<String>(nickWatchList); + } + if (current.size() > maxListSize) + { + // cut off list to maximum number of entries allowed by server + current = current.subList(0, maxListSize); + } + final int maxLength = 510 - WATCH_ADD_CMD_STATIC_OVERHEAD; + final StringBuilder query = new StringBuilder(); + for (String nick : current) + { + if (query.length() + nick.length() + 2 > maxLength) + { + // full payload, send monitor query now + irc.rawMessage("WATCH " + query); + query.delete(0, query.length()); + } + else if (query.length() > 0) + { + query.append(" "); + } + query.append('+').append(nick); + } + if (query.length() > 0) + { + // send query for remaining nicks + irc.rawMessage("WATCH " + query); + } + } + + @Override + public void add(final String nick) + { + LOGGER.trace("Adding nick '" + nick + "' to WATCH watch list."); + this.nickWatchList.add(nick); + this.irc.rawMessage("WATCH +" + nick); + } + + @Override + public void remove(final String nick) + { + LOGGER.trace("Removing nick '" + nick + "' from WATCH watch list."); + this.nickWatchList.remove(nick); + this.irc.rawMessage("WATCH -" + nick); + } + + /** + * Listener for WATCH replies. + * + * @author Danny van Heumen + */ + private final class WatchReplyListener + extends AbstractIrcMessageListener + { + /** + * Numeric message id for notification that user logged on. + */ + private static final int IRC_RPL_LOGON = 600; + + /** + * Numeric message id for notification that user logged off. + */ + private static final int IRC_RPL_LOGOFF = 601; + + /** + * Numeric message id for when nick is removed from WATCH list. + */ + private static final int IRC_RPL_WATCHOFF = 602; + + /** + * Numeric message id for ONLINE nick response. + */ + private static final int IRC_RPL_NOWON = 604; + + /** + * Numeric message id for OFFLINE nick response. + */ + private static final int IRC_RPL_NOWOFF = 605; + + // /** + // * Numeric message id for MONLIST entry. + // */ + // private static final int IRC_RPL_MONLIST = 732; + // + // /** + // * Numeric message id for ENDOFMONLIST. + // */ + // private static final int IRC_RPL_ENDOFMONLIST = 733; + + /** + * Error message signaling full list. Nick list provided are all nicks + * that failed to be added to the WATCH list. + */ + private static final int IRC_ERR_LISTFULL = 512; + + /** + * Operation set persistent presence instance. + */ + private final OperationSetPersistentPresenceIrcImpl operationSet; + + /** + * Set of nicks that are confirmed to be monitored by the server. + */ + private final Set<String> monitoredNickList; + + // TODO Update to act on onClientError once available. + + /** + * Constructor. + * + * @param monitored SYNCHRONIZED Collection of monitored nicks. This + * collection will be updated with all nicks that are + * confirmed to be subscribed by the WATCH command. + * @param operationSet the persistent presence opset used to update nick + * presence statuses. + */ + public WatchReplyListener(final Set<String> monitored, + final OperationSetPersistentPresenceIrcImpl operationSet) + { + super(WatchPresenceWatcher.this.irc, + WatchPresenceWatcher.this.connectionState); + if (operationSet == null) + { + throw new IllegalArgumentException( + "operationSet cannot be null"); + } + this.operationSet = operationSet; + if (monitored == null) + { + throw new IllegalArgumentException("monitored cannot be null"); + } + this.monitoredNickList = monitored; + } + + /** + * Numeric messages received in response to WATCH commands or presence + * updates. + */ + @Override + public void onServerNumericMessage(final ServerNumericMessage msg) + { + final String nick; + switch (msg.getNumericCode()) + { + case IRC_RPL_NOWON: + nick = parseWatchResponse(msg.getText()); + monitoredNickList.add(nick); + update(nick, IrcStatusEnum.ONLINE); + break; + case IRC_RPL_LOGON: + nick = parseWatchResponse(msg.getText()); + update(nick, IrcStatusEnum.ONLINE); + break; + case IRC_RPL_NOWOFF: + nick = parseWatchResponse(msg.getText()); + monitoredNickList.add(nick); + update(nick, IrcStatusEnum.OFFLINE); + break; + case IRC_RPL_LOGOFF: + nick = parseWatchResponse(msg.getText()); + update(nick, IrcStatusEnum.OFFLINE); + break; + case IRC_RPL_WATCHOFF: + nick = parseWatchResponse(msg.getText()); + monitoredNickList.remove(nick); + break; + case IRC_ERR_LISTFULL: + LOGGER.debug("WATCH list is full. Nick was not added. " + + "Fall back Basic Poller will be used if it is enabled. (" + + msg.getText() + ")"); + break; + } + } + + /** + * Update all monitored nicks upon receiving a server-side QUIT message + * for local user. + */ + @Override + public void onUserQuit(QuitMessage msg) + { + super.onUserQuit(msg); + if (localUser(msg.getSource().getNick())) { + updateAll(IrcStatusEnum.OFFLINE); + } + } + + /** + * Update all monitored nicks upon receiving a server-side ERROR + * response. + */ + @Override + public void onError(ErrorMessage msg) + { + super.onError(msg); + updateAll(IrcStatusEnum.OFFLINE); + } + + /** + * Parse response messages. + * + * @param message the message + * @return Returns the list of targets extracted. + */ + private String parseWatchResponse(final String message) + { + final String[] parts = message.split(" "); + return parts[0]; + } + + /** + * Update all monitored nicks to specified status. + * + * @param status the desired status + */ + private void updateAll(final IrcStatusEnum status) + { + final LinkedList<String> nicks; + synchronized (monitoredNickList) + { + nicks = new LinkedList<String>(monitoredNickList); + } + for (String nick : nicks) + { + update(nick, status); + } + } + + /** + * Update specified nick to specified presence status. + * + * @param nick the target nick + * @param status the current status + */ + private void update(final String nick, final IrcStatusEnum status) + { + this.operationSet.updateNickContactPresence(nick, status); + } + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/irc/collection/DynamicDifferenceSet.java b/src/net/java/sip/communicator/impl/protocol/irc/collection/DynamicDifferenceSet.java new file mode 100644 index 0000000..4f8daca --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/irc/collection/DynamicDifferenceSet.java @@ -0,0 +1,207 @@ +/* + * 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.collection; + +import java.util.*; + +/** + * Dynamic difference set. + * + * A custom implementation that constructs a (dynamic) difference set from 2 + * input instances. The first input instance is the 'source' which is used as + * the base data set. The second input is the 'removals', which are then removed + * from the source set. The dynamic set is computed each time a method call is + * executed. + * + * This set is immutable. That is, the modifier methods are unsupported. Changes + * that do happen are derived from changes in the 'source' set or the 'removals' + * set. + * + * Keep in mind that any operation on this dynamic returns a result that is only + * valid in the moment. Even calling two subsequent methods can result in + * difference or conflicting results. + * + * @author Danny van Heumen + * + * @param <E> The type of element that is stored in the set. + */ +public class DynamicDifferenceSet<E> + implements Set<E> +{ + /** + * SYNCHRONIZED The source or base data set. This set is the basis and contains all the + * elements that can possibly be in the dynamic set. + */ + private final Set<E> source; + + /** + * SYNCHRONIZED The removals set contains elements that are removed during the + * calculation of the difference set at the moment. + */ + private final Set<E> removals; + + /** + * Constructor for creating a difference set instance. + * + * @param source The source data set. + * @param removals The removals data set which will be used to create the + * difference. + */ + public DynamicDifferenceSet(final Set<E> source, + final Set<E> removals) + { + if (source == null) + { + throw new IllegalArgumentException("source cannot be null"); + } + this.source = source; + if (removals == null) + { + throw new IllegalArgumentException("removals cannot be null"); + } + this.removals = removals; + } + + /** + * Calculate the difference set based on the current state of the data. + * + * @return Returns the current difference set that is calculated on the fly. + */ + private Set<E> calculate() + { + final TreeSet<E> current; + synchronized (source) + { + current = new TreeSet<E>(source); + } + synchronized (removals) + { + current.removeAll(removals); + } + return current; + } + + /** + * Get the size of the difference set. (Keep in mind that these numbers are + * only momentary and may be obsolete as soon as they are calculated.) + */ + @Override + public int size() + { + return calculate().size(); + } + + /** + * Check if the difference set of the moment is empty. (Keep in mind that + * the result may be obsolete as soon as it is calculated.) + */ + @Override + public boolean isEmpty() + { + return calculate().isEmpty(); + } + + /** + * Check if an element is contained in the difference set. + */ + @Override + public boolean contains(Object o) + { + return this.source.contains(o) && !this.removals.contains(o); + } + + /** + * Get an iterator for the difference set of the moment. + */ + @Override + public Iterator<E> iterator() + { + return calculate().iterator(); + } + + /** + * Get an array of the current difference set. + */ + @Override + public Object[] toArray() + { + return calculate().toArray(); + } + + /** + * Get an array of the current difference set. + */ + @Override + public <T> T[] toArray(T[] a) + { + return calculate().toArray(a); + } + + /** + * Adding elements to a difference set is unsupported. + */ + @Override + public boolean add(E e) + { + throw new UnsupportedOperationException(); + } + + /** + * Removing elements from a difference set is unsupported. + */ + @Override + public boolean remove(Object o) + { + throw new UnsupportedOperationException(); + } + + /** + * Check if all provided elements are contained in the difference set of the + * moment. + */ + @Override + public boolean containsAll(Collection<?> c) + { + return calculate().containsAll(c); + } + + /** + * Adding elements to a difference set is unsupported. + */ + @Override + public boolean addAll(Collection<? extends E> c) + { + throw new UnsupportedOperationException(); + } + + /** + * Removing elements from a difference set is unsupported. + */ + @Override + public boolean removeAll(Collection<?> c) + { + throw new UnsupportedOperationException(); + } + + /** + * Modifying a difference set is unsupported. + */ + @Override + public boolean retainAll(Collection<?> c) + { + throw new UnsupportedOperationException(); + } + + /** + * Modifying a difference set is unsupported. + */ + @Override + public void clear() + { + throw new UnsupportedOperationException(); + } +} diff --git a/src/net/java/sip/communicator/impl/protocol/irc/collection/package-info.java b/src/net/java/sip/communicator/impl/protocol/irc/collection/package-info.java new file mode 100644 index 0000000..850262e --- /dev/null +++ b/src/net/java/sip/communicator/impl/protocol/irc/collection/package-info.java @@ -0,0 +1,13 @@ +/* + * Jitsi, the OpenSource Java VoIP and Instant Messaging client. + * + * Distributable under LGPL license. + * See terms of license at gnu.org. + */ +/** + * Set of dynamic collections that are used to manage the interaction between + * various individual components. + * + * @author Danny van Heumen + */ +package net.java.sip.communicator.impl.protocol.irc.collection;
\ No newline at end of file |