aboutsummaryrefslogtreecommitdiffstats
path: root/src/net/java/sip/communicator/impl
diff options
context:
space:
mode:
authorBoris Grozev <boris@jitsi.org>2015-03-20 12:28:10 -0700
committerBoris Grozev <boris@jitsi.org>2015-03-20 12:28:10 -0700
commit100b6b88765c3aaddcb2f63785133bccb2b96423 (patch)
tree1452ae24ca3d608ba3cb27d1376e6712a895dd61 /src/net/java/sip/communicator/impl
parentec99032d9093443d1fb461125d510ef3d47d0af8 (diff)
parentdcc1026578286af31ba883cec09f6383220e8a77 (diff)
downloadjitsi-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')
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/BasicPollerPresenceWatcher.java564
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/ContactGroupIrcImpl.java85
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/ContactIrcImpl.java35
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/ISupport.java10
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/IrcStack.java10
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/MonitorPresenceWatcher.java345
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/OperationSetPersistentPresenceIrcImpl.java227
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/PresenceManager.java717
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/PresenceWatcher.java32
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/WatchPresenceWatcher.java339
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/collection/DynamicDifferenceSet.java207
-rw-r--r--src/net/java/sip/communicator/impl/protocol/irc/collection/package-info.java13
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