Merge branch 'merge-me-after-the-bugfix-release'

Conflicts:
	lib/installer-exclude/libjitsi.jar
maven
Boris Grozev 12 years ago
commit 100b6b8876

Binary file not shown.

@ -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);
}
}
}
}

@ -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>();
/**
@ -45,12 +45,17 @@ 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 ContactGroupIrcImpl(final ProtocolProviderServiceIrcImpl provider,
throw new IllegalArgumentException("name cannot be null");
}
this.name = name;
this.persistent = true;
}
/**
@ -166,7 +172,7 @@ public int countContacts()
@Override
public ContactIrcImpl getContact(final String id)
{
if (id == null)
if (id == null || id.isEmpty())
{
return null;
}
@ -180,6 +186,34 @@ public ContactIrcImpl getContact(final String id)
return null;
}
/**
* 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.
*
@ -246,7 +280,18 @@ public ContactGroup getParentContactGroup()
@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 String getUID()
@Override
public boolean isResolved()
{
return false;
return true;
}
/**
@ -296,6 +341,20 @@ public void addContact(final ContactIrcImpl contact)
this.contacts.add(contact);
}
/**
* 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.
*
@ -309,4 +368,18 @@ public void addSubGroup(final ContactGroupIrcImpl group)
}
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);
}
}

@ -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 ContactIrcImpl(final ProtocolProviderServiceIrcImpl provider,
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;
}
/**
@ -150,6 +156,20 @@ public ContactGroup getParentContactGroup()
return this.parentGroup;
}
/**
* 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.
*
@ -163,15 +183,18 @@ public ProtocolProviderService getProtocolProvider()
/**
* 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 boolean isResolved()
// 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;
}
/**

@ -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.

@ -220,6 +220,16 @@ public IrcConnection getConnection()
return this.session.get();
}
/**
* 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.
*

@ -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);
}
}
}

@ -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 @@ private ContactIrcImpl createVolatileContact(final String id)
// 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 @@ private ContactIrcImpl createVolatileContact(final String id)
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 @@ private ContactGroupIrcImpl getNonPersistentGroup()
ContactGroupIrcImpl volatileGroup =
new ContactGroupIrcImpl(this.parentProvider, this.rootGroup,
groupName);
volatileGroup.setPersistent(false);
this.rootGroup.addSubGroup(volatileGroup);
@ -132,18 +137,7 @@ public void subscribe(final String contactIdentifier)
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 void subscribe(final ContactGroup parent,
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 void unsubscribe(final Contact contact)
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 void createServerStoredContactGroup(final ContactGroup parent,
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 void removeServerStoredContactGroup(final ContactGroup group)
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 void moveContactToGroup(final Contact contactToMove,
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 void moveContactToGroup(final Contact contactToMove,
@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 ContactGroup getServerStoredContactListRoot()
*
* @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 ContactIrcImpl createUnresolvedContact(final String address,
*
* @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 ContactGroupIrcImpl createUnresolvedContactGroup(
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 Iterator<PresenceStatus> getSupportedStatusSet()
}
/**
* 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 PresenceStatus queryContactStatus(final String contactIdentifier)
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 PresenceStatus queryContactStatus(final String contactIdentifier)
@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);
}
/**

@ -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
*/
@ -40,16 +42,6 @@ public class PresenceManager
private static final Logger LOGGER = Logger
.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.
*
@ -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.
@ -79,6 +71,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.
*/
@ -132,40 +144,90 @@ public PresenceManager(final IRCApi irc, final IIRCState connectionState,
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 @@ private Integer parseISupportAwayLen(final IIRCState state)
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 @@ private Integer parseISupportAwayLen(final IIRCState state)
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;
}
}
/**
@ -265,6 +405,47 @@ private String verifyMessage(final String message)
return message;
}
/**
* 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.
*
@ -272,7 +453,10 @@ private String verifyMessage(final String message)
*/
public void addNickWatch(final String nick)
{
this.nickWatchList.add(nick);
if (this.watcher != null)
{
this.watcher.add(nick);
}
}
/**
@ -282,7 +466,10 @@ public void addNickWatch(final String nick)
*/
public void removeNickWatch(final String nick)
{
this.nickWatchList.remove(nick);
if (this.watcher != null)
{
this.watcher.remove(nick);
}
}
/**
@ -291,7 +478,7 @@ public void removeNickWatch(final String nick)
*
* @author Danny van Heumen
*/
private final class PresenceListener
private final class LocalUserPresenceListener
extends AbstractIrcMessageListener
{
/**
@ -308,7 +495,7 @@ private final class PresenceListener
/**
* Constructor.
*/
public PresenceListener()
public LocalUserPresenceListener()
{
super(PresenceManager.this.irc,
PresenceManager.this.connectionState);
@ -356,307 +543,104 @@ public void onServerNumericMessage(final ServerNumericMessage msg)
}
/**
* 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 void onServerNumericMessage(final ServerNumericMessage msg)
}
/**
* 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();
}
}
}

@ -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);
}

@ -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);
}
}
}

@ -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();
}
}

@ -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;
Loading…
Cancel
Save