diff options
4 files changed, 439 insertions, 34 deletions
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 86ee6e4..66e9895 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/ISupport.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/ISupport.java @@ -43,7 +43,11 @@ public enum ISupport /** * Maximum number of entries in the MONITOR list supported by this server. */ - MONITOR; + 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/MonitorPresenceWatcher.java b/src/net/java/sip/communicator/impl/protocol/irc/MonitorPresenceWatcher.java index b4f41e8..31e1405 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/MonitorPresenceWatcher.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/MonitorPresenceWatcher.java @@ -141,6 +141,7 @@ class MonitorPresenceWatcher LOGGER.trace("Removing nick '" + nick + "' from MONITOR watch list."); this.nickWatchList.remove(nick); this.irc.rawMessage("MONITOR - " + nick); + // FIXME Also remove from monitoredNicks list! } /** 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 5873e46..4f658bd 100644 --- a/src/net/java/sip/communicator/impl/protocol/irc/PresenceManager.java +++ b/src/net/java/sip/communicator/impl/protocol/irc/PresenceManager.java @@ -84,6 +84,16 @@ public class PresenceManager 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 = @@ -152,42 +162,59 @@ public class PresenceManager // TODO move parse methods to ISupport enum type this.isupportAwayLen = parseISupportAwayLen(this.connectionState); this.isupportMonitor = parseISupportMonitor(this.connectionState); - if (config.isContactPresenceTaskEnabled()) + this.isupportWatch = parseISupportWatch(this.connectionState); + if (this.isupportMonitor != null) { - if (this.isupportMonitor == null) - { - this.watcher = - new BasicPollerPresenceWatcher(this.irc, - this.connectionState, this.operationSet, nickWatchList, - this.serverIdentity); - } - else - { - // 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); - // 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); - } + // 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); + // FIXME only set up basic poller if option enabled? + // 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 + else if (this.isupportWatch != null) + { + // 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); + // FIXME only set up basic poller if option enabled? + // 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 (config.isContactPresenceTaskEnabled()) { - // FIXME replace with NOOP watcher for code simplicity? + this.watcher = + new BasicPollerPresenceWatcher(this.irc, this.connectionState, + this.operationSet, nickWatchList, this.serverIdentity); + } else { this.watcher = null; } } @@ -260,6 +287,40 @@ public class PresenceManager } /** + * 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; + } + } + + /** * Check current Away state. * * @return returns <tt>true</tt> if away or <tt>false</tt> if not away 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); + } + } +} |