/* * 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 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 nickWatchList, final AtomicReference 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 serverIdentity) { // FIFO query list to be shared between presence watcher task and // presence reply listener. final List> queryList = Collections.synchronizedList(new LinkedList>()); 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 watchList; /** * FIFO list storing each ISON query that is sent, for use when * responses return. */ private final List> queryList; /** * Reference to the current server identity. */ private final AtomicReference 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 watchList, final List> queryList, final AtomicReference 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 list; synchronized (this.watchList) { list = new LinkedList(this.watchList); } LinkedList nicks = new LinkedList(); // 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(); } 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> 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> 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 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(); } 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 list; synchronized (BasicPollerPresenceWatcher.this.nickWatchList) { list = new LinkedList( BasicPollerPresenceWatcher.this.nickWatchList); } for (String nick : list) { BasicPollerPresenceWatcher.this.operationSet .updateNickContactPresence(nick, status); } } } }