diff options
Diffstat (limited to 'src/org/jivesoftware/smack/Roster.java')
-rw-r--r-- | src/org/jivesoftware/smack/Roster.java | 1038 |
1 files changed, 1038 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smack/Roster.java b/src/org/jivesoftware/smack/Roster.java new file mode 100644 index 0000000..66a78b2 --- /dev/null +++ b/src/org/jivesoftware/smack/Roster.java @@ -0,0 +1,1038 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2007 Jive Software. + * + * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.smack; + +import org.jivesoftware.smack.filter.PacketFilter; +import org.jivesoftware.smack.filter.PacketIDFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.util.StringUtils; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Represents a user's roster, which is the collection of users a person receives + * presence updates for. Roster items are categorized into groups for easier management.<p> + * <p/> + * Others users may attempt to subscribe to this user using a subscription request. Three + * modes are supported for handling these requests: <ul> + * <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li> + * <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li> + * <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li> + * </ul> + * + * @author Matt Tucker + * @see Connection#getRoster() + */ +public class Roster { + + /** + * The default subscription processing mode to use when a Roster is created. By default + * all subscription requests are automatically accepted. + */ + private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.accept_all; + private RosterStorage persistentStorage; + + private Connection connection; + private final Map<String, RosterGroup> groups; + private final Map<String,RosterEntry> entries; + private final List<RosterEntry> unfiledEntries; + private final List<RosterListener> rosterListeners; + private Map<String, Map<String, Presence>> presenceMap; + // The roster is marked as initialized when at least a single roster packet + // has been received and processed. + boolean rosterInitialized = false; + private PresencePacketListener presencePacketListener; + + private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode(); + + private String requestPacketId; + + /** + * Returns the default subscription processing mode to use when a new Roster is created. The + * subscription processing mode dictates what action Smack will take when subscription + * requests from other users are made. The default subscription mode + * is {@link SubscriptionMode#accept_all}. + * + * @return the default subscription mode to use for new Rosters + */ + public static SubscriptionMode getDefaultSubscriptionMode() { + return defaultSubscriptionMode; + } + + /** + * Sets the default subscription processing mode to use when a new Roster is created. The + * subscription processing mode dictates what action Smack will take when subscription + * requests from other users are made. The default subscription mode + * is {@link SubscriptionMode#accept_all}. + * + * @param subscriptionMode the default subscription mode to use for new Rosters. + */ + public static void setDefaultSubscriptionMode(SubscriptionMode subscriptionMode) { + defaultSubscriptionMode = subscriptionMode; + } + + Roster(final Connection connection, RosterStorage persistentStorage){ + this(connection); + this.persistentStorage = persistentStorage; + } + + /** + * Creates a new roster. + * + * @param connection an XMPP connection. + */ + Roster(final Connection connection) { + this.connection = connection; + //Disable roster versioning if server doesn't offer support for it + if(!connection.getConfiguration().isRosterVersioningAvailable()){ + persistentStorage=null; + } + groups = new ConcurrentHashMap<String, RosterGroup>(); + unfiledEntries = new CopyOnWriteArrayList<RosterEntry>(); + entries = new ConcurrentHashMap<String,RosterEntry>(); + rosterListeners = new CopyOnWriteArrayList<RosterListener>(); + presenceMap = new ConcurrentHashMap<String, Map<String, Presence>>(); + // Listen for any roster packets. + PacketFilter rosterFilter = new PacketTypeFilter(RosterPacket.class); + connection.addPacketListener(new RosterPacketListener(), rosterFilter); + // Listen for any presence packets. + PacketFilter presenceFilter = new PacketTypeFilter(Presence.class); + presencePacketListener = new PresencePacketListener(); + connection.addPacketListener(presencePacketListener, presenceFilter); + + // Listen for connection events + final ConnectionListener connectionListener = new AbstractConnectionListener() { + + public void connectionClosed() { + // Changes the presence available contacts to unavailable + setOfflinePresences(); + } + + public void connectionClosedOnError(Exception e) { + // Changes the presence available contacts to unavailable + setOfflinePresences(); + } + + }; + + // if not connected add listener after successful login + if(!this.connection.isConnected()) { + Connection.addConnectionCreationListener(new ConnectionCreationListener() { + + public void connectionCreated(Connection connection) { + if(connection.equals(Roster.this.connection)) { + Roster.this.connection.addConnectionListener(connectionListener); + } + + } + }); + } else { + connection.addConnectionListener(connectionListener); + } + } + + /** + * Returns the subscription processing mode, which dictates what action + * Smack will take when subscription requests from other users are made. + * The default subscription mode is {@link SubscriptionMode#accept_all}.<p> + * <p/> + * If using the manual mode, a PacketListener should be registered that + * listens for Presence packets that have a type of + * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. + * + * @return the subscription mode. + */ + public SubscriptionMode getSubscriptionMode() { + return subscriptionMode; + } + + /** + * Sets the subscription processing mode, which dictates what action + * Smack will take when subscription requests from other users are made. + * The default subscription mode is {@link SubscriptionMode#accept_all}.<p> + * <p/> + * If using the manual mode, a PacketListener should be registered that + * listens for Presence packets that have a type of + * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. + * + * @param subscriptionMode the subscription mode. + */ + public void setSubscriptionMode(SubscriptionMode subscriptionMode) { + this.subscriptionMode = subscriptionMode; + } + + /** + * Reloads the entire roster from the server. This is an asynchronous operation, + * which means the method will return immediately, and the roster will be + * reloaded at a later point when the server responds to the reload request. + * + * @throws IllegalStateException if connection is not logged in or logged in anonymously + */ + public void reload() { + if (!connection.isAuthenticated()) { + throw new IllegalStateException("Not logged in to server."); + } + if (connection.isAnonymous()) { + throw new IllegalStateException("Anonymous users can't have a roster."); + } + + RosterPacket packet = new RosterPacket(); + if(persistentStorage!=null){ + packet.setVersion(persistentStorage.getRosterVersion()); + } + requestPacketId = packet.getPacketID(); + PacketFilter idFilter = new PacketIDFilter(requestPacketId); + connection.addPacketListener(new RosterResultListener(), idFilter); + connection.sendPacket(packet); + } + + /** + * Adds a listener to this roster. The listener will be fired anytime one or more + * changes to the roster are pushed from the server. + * + * @param rosterListener a roster listener. + */ + public void addRosterListener(RosterListener rosterListener) { + if (!rosterListeners.contains(rosterListener)) { + rosterListeners.add(rosterListener); + } + } + + /** + * Removes a listener from this roster. The listener will be fired anytime one or more + * changes to the roster are pushed from the server. + * + * @param rosterListener a roster listener. + */ + public void removeRosterListener(RosterListener rosterListener) { + rosterListeners.remove(rosterListener); + } + + /** + * Creates a new group.<p> + * <p/> + * Note: you must add at least one entry to the group for the group to be kept + * after a logout/login. This is due to the way that XMPP stores group information. + * + * @param name the name of the group. + * @return a new group. + * @throws IllegalStateException if connection is not logged in or logged in anonymously + */ + public RosterGroup createGroup(String name) { + if (!connection.isAuthenticated()) { + throw new IllegalStateException("Not logged in to server."); + } + if (connection.isAnonymous()) { + throw new IllegalStateException("Anonymous users can't have a roster."); + } + if (groups.containsKey(name)) { + throw new IllegalArgumentException("Group with name " + name + " alread exists."); + } + + RosterGroup group = new RosterGroup(name, connection); + groups.put(name, group); + return group; + } + + /** + * Creates a new roster entry and presence subscription. The server will asynchronously + * update the roster with the subscription status. + * + * @param user the user. (e.g. johndoe@jabber.org) + * @param name the nickname of the user. + * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the + * the roster entry won't belong to a group. + * @throws XMPPException if an XMPP exception occurs. + * @throws IllegalStateException if connection is not logged in or logged in anonymously + */ + public void createEntry(String user, String name, String[] groups) throws XMPPException { + if (!connection.isAuthenticated()) { + throw new IllegalStateException("Not logged in to server."); + } + if (connection.isAnonymous()) { + throw new IllegalStateException("Anonymous users can't have a roster."); + } + + // Create and send roster entry creation packet. + RosterPacket rosterPacket = new RosterPacket(); + rosterPacket.setType(IQ.Type.SET); + RosterPacket.Item item = new RosterPacket.Item(user, name); + if (groups != null) { + for (String group : groups) { + if (group != null && group.trim().length() > 0) { + item.addGroupName(group); + } + } + } + rosterPacket.addRosterItem(item); + // Wait up to a certain number of seconds for a reply from the server. + PacketCollector collector = connection.createPacketCollector( + new PacketIDFilter(rosterPacket.getPacketID())); + connection.sendPacket(rosterPacket); + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + + // Create a presence subscription packet and send. + Presence presencePacket = new Presence(Presence.Type.subscribe); + presencePacket.setTo(user); + connection.sendPacket(presencePacket); + } + + private void insertRosterItems(List<RosterPacket.Item> items){ + Collection<String> addedEntries = new ArrayList<String>(); + Collection<String> updatedEntries = new ArrayList<String>(); + Collection<String> deletedEntries = new ArrayList<String>(); + Iterator<RosterPacket.Item> iter = items.iterator(); + while(iter.hasNext()){ + insertRosterItem(iter.next(), addedEntries,updatedEntries,deletedEntries); + } + fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); + } + + private void insertRosterItem(RosterPacket.Item item, Collection<String> addedEntries, + Collection<String> updatedEntries, Collection<String> deletedEntries){ + RosterEntry entry = new RosterEntry(item.getUser(), item.getName(), + item.getItemType(), item.getItemStatus(), this, connection); + + // If the packet is of the type REMOVE then remove the entry + if (RosterPacket.ItemType.remove.equals(item.getItemType())) { + // Remove the entry from the entry list. + if (entries.containsKey(item.getUser())) { + entries.remove(item.getUser()); + } + // Remove the entry from the unfiled entry list. + if (unfiledEntries.contains(entry)) { + unfiledEntries.remove(entry); + } + // Removing the user from the roster, so remove any presence information + // about them. + String key = StringUtils.parseName(item.getUser()) + "@" + + StringUtils.parseServer(item.getUser()); + presenceMap.remove(key); + // Keep note that an entry has been removed + if(deletedEntries!=null){ + deletedEntries.add(item.getUser()); + } + } + else { + // Make sure the entry is in the entry list. + if (!entries.containsKey(item.getUser())) { + entries.put(item.getUser(), entry); + // Keep note that an entry has been added + if(addedEntries!=null){ + addedEntries.add(item.getUser()); + } + } + else { + // If the entry was in then list then update its state with the new values + entries.put(item.getUser(), entry); + + // Keep note that an entry has been updated + if(updatedEntries!=null){ + updatedEntries.add(item.getUser()); + } + } + // If the roster entry belongs to any groups, remove it from the + // list of unfiled entries. + if (!item.getGroupNames().isEmpty()) { + unfiledEntries.remove(entry); + } + // Otherwise add it to the list of unfiled entries. + else { + if (!unfiledEntries.contains(entry)) { + unfiledEntries.add(entry); + } + } + } + + // Find the list of groups that the user currently belongs to. + List<String> currentGroupNames = new ArrayList<String>(); + for (RosterGroup group: getGroups()) { + if (group.contains(entry)) { + currentGroupNames.add(group.getName()); + } + } + + // If the packet is not of the type REMOVE then add the entry to the groups + if (!RosterPacket.ItemType.remove.equals(item.getItemType())) { + // Create the new list of groups the user belongs to. + List<String> newGroupNames = new ArrayList<String>(); + for (String groupName : item.getGroupNames()) { + // Add the group name to the list. + newGroupNames.add(groupName); + + // Add the entry to the group. + RosterGroup group = getGroup(groupName); + if (group == null) { + group = createGroup(groupName); + groups.put(groupName, group); + } + // Add the entry. + group.addEntryLocal(entry); + } + + // We have the list of old and new group names. We now need to + // remove the entry from the all the groups it may no longer belong + // to. We do this by subracting the new group set from the old. + for (String newGroupName : newGroupNames) { + currentGroupNames.remove(newGroupName); + } + } + + // Loop through any groups that remain and remove the entries. + // This is neccessary for the case of remote entry removals. + for (String groupName : currentGroupNames) { + RosterGroup group = getGroup(groupName); + group.removeEntryLocal(entry); + if (group.getEntryCount() == 0) { + groups.remove(groupName); + } + } + // Remove all the groups with no entries. We have to do this because + // RosterGroup.removeEntry removes the entry immediately (locally) and the + // group could remain empty. + // TODO Check the performance/logic for rosters with large number of groups + for (RosterGroup group : getGroups()) { + if (group.getEntryCount() == 0) { + groups.remove(group.getName()); + } + } + } + + /** + * Removes a roster entry from the roster. The roster entry will also be removed from the + * unfiled entries or from any roster group where it could belong and will no longer be part + * of the roster. Note that this is an asynchronous call -- Smack must wait for the server + * to send an updated subscription status. + * + * @param entry a roster entry. + * @throws XMPPException if an XMPP error occurs. + * @throws IllegalStateException if connection is not logged in or logged in anonymously + */ + public void removeEntry(RosterEntry entry) throws XMPPException { + if (!connection.isAuthenticated()) { + throw new IllegalStateException("Not logged in to server."); + } + if (connection.isAnonymous()) { + throw new IllegalStateException("Anonymous users can't have a roster."); + } + + // Only remove the entry if it's in the entry list. + // The actual removal logic takes place in RosterPacketListenerprocess>>Packet(Packet) + if (!entries.containsKey(entry.getUser())) { + return; + } + RosterPacket packet = new RosterPacket(); + packet.setType(IQ.Type.SET); + RosterPacket.Item item = RosterEntry.toRosterItem(entry); + // Set the item type as REMOVE so that the server will delete the entry + item.setItemType(RosterPacket.ItemType.remove); + packet.addRosterItem(item); + PacketCollector collector = connection.createPacketCollector( + new PacketIDFilter(packet.getPacketID())); + connection.sendPacket(packet); + IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); + collector.cancel(); + if (response == null) { + throw new XMPPException("No response from the server."); + } + // If the server replied with an error, throw an exception. + else if (response.getType() == IQ.Type.ERROR) { + throw new XMPPException(response.getError()); + } + } + + /** + * Returns a count of the entries in the roster. + * + * @return the number of entries in the roster. + */ + public int getEntryCount() { + return getEntries().size(); + } + + /** + * Returns an unmodifiable collection of all entries in the roster, including entries + * that don't belong to any groups. + * + * @return all entries in the roster. + */ + public Collection<RosterEntry> getEntries() { + Set<RosterEntry> allEntries = new HashSet<RosterEntry>(); + // Loop through all roster groups and add their entries to the answer + for (RosterGroup rosterGroup : getGroups()) { + allEntries.addAll(rosterGroup.getEntries()); + } + // Add the roster unfiled entries to the answer + allEntries.addAll(unfiledEntries); + + return Collections.unmodifiableCollection(allEntries); + } + + /** + * Returns a count of the unfiled entries in the roster. An unfiled entry is + * an entry that doesn't belong to any groups. + * + * @return the number of unfiled entries in the roster. + */ + public int getUnfiledEntryCount() { + return unfiledEntries.size(); + } + + /** + * Returns an unmodifiable collection for the unfiled roster entries. An unfiled entry is + * an entry that doesn't belong to any groups. + * + * @return the unfiled roster entries. + */ + public Collection<RosterEntry> getUnfiledEntries() { + return Collections.unmodifiableList(unfiledEntries); + } + + /** + * Returns the roster entry associated with the given XMPP address or + * <tt>null</tt> if the user is not an entry in the roster. + * + * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be + * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource"). + * @return the roster entry or <tt>null</tt> if it does not exist. + */ + public RosterEntry getEntry(String user) { + if (user == null) { + return null; + } + return entries.get(user.toLowerCase()); + } + + /** + * Returns true if the specified XMPP address is an entry in the roster. + * + * @param user the XMPP address of the user (eg "jsmith@example.com"). The + * address could be in any valid format (e.g. "domain/resource", + * "user@domain" or "user@domain/resource"). + * @return true if the XMPP address is an entry in the roster. + */ + public boolean contains(String user) { + return getEntry(user) != null; + } + + /** + * Returns the roster group with the specified name, or <tt>null</tt> if the + * group doesn't exist. + * + * @param name the name of the group. + * @return the roster group with the specified name. + */ + public RosterGroup getGroup(String name) { + return groups.get(name); + } + + /** + * Returns the number of the groups in the roster. + * + * @return the number of groups in the roster. + */ + public int getGroupCount() { + return groups.size(); + } + + /** + * Returns an unmodifiable collections of all the roster groups. + * + * @return an iterator for all roster groups. + */ + public Collection<RosterGroup> getGroups() { + return Collections.unmodifiableCollection(groups.values()); + } + + /** + * Returns the presence info for a particular user. If the user is offline, or + * if no presence data is available (such as when you are not subscribed to the + * user's presence updates), unavailable presence will be returned.<p> + * <p/> + * If the user has several presences (one for each resource), then the presence with + * highest priority will be returned. If multiple presences have the same priority, + * the one with the "most available" presence mode will be returned. In order, + * that's {@link org.jivesoftware.smack.packet.Presence.Mode#chat free to chat}, + * {@link org.jivesoftware.smack.packet.Presence.Mode#available available}, + * {@link org.jivesoftware.smack.packet.Presence.Mode#away away}, + * {@link org.jivesoftware.smack.packet.Presence.Mode#xa extended away}, and + * {@link org.jivesoftware.smack.packet.Presence.Mode#dnd do not disturb}.<p> + * <p/> + * Note that presence information is received asynchronously. So, just after logging + * in to the server, presence values for users in the roster may be unavailable + * even if they are actually online. In other words, the value returned by this + * method should only be treated as a snapshot in time, and may not accurately reflect + * other user's presence instant by instant. If you need to track presence over time, + * such as when showing a visual representation of the roster, consider using a + * {@link RosterListener}. + * + * @param user an XMPP ID. The address could be in any valid format (e.g. + * "domain/resource", "user@domain" or "user@domain/resource"). Any resource + * information that's part of the ID will be discarded. + * @return the user's current presence, or unavailable presence if the user is offline + * or if no presence information is available.. + */ + public Presence getPresence(String user) { + String key = getPresenceMapKey(StringUtils.parseBareAddress(user)); + Map<String, Presence> userPresences = presenceMap.get(key); + if (userPresences == null) { + Presence presence = new Presence(Presence.Type.unavailable); + presence.setFrom(user); + return presence; + } + else { + // Find the resource with the highest priority + // Might be changed to use the resource with the highest availability instead. + Presence presence = null; + + for (String resource : userPresences.keySet()) { + Presence p = userPresences.get(resource); + if (!p.isAvailable()) { + continue; + } + // Chose presence with highest priority first. + if (presence == null || p.getPriority() > presence.getPriority()) { + presence = p; + } + // If equal priority, choose "most available" by the mode value. + else if (p.getPriority() == presence.getPriority()) { + Presence.Mode pMode = p.getMode(); + // Default to presence mode of available. + if (pMode == null) { + pMode = Presence.Mode.available; + } + Presence.Mode presenceMode = presence.getMode(); + // Default to presence mode of available. + if (presenceMode == null) { + presenceMode = Presence.Mode.available; + } + if (pMode.compareTo(presenceMode) < 0) { + presence = p; + } + } + } + if (presence == null) { + presence = new Presence(Presence.Type.unavailable); + presence.setFrom(user); + return presence; + } + else { + return presence; + } + } + } + + /** + * Returns the presence info for a particular user's resource, or unavailable presence + * if the user is offline or if no presence information is available, such as + * when you are not subscribed to the user's presence updates. + * + * @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource). + * @return the user's current presence, or unavailable presence if the user is offline + * or if no presence information is available. + */ + public Presence getPresenceResource(String userWithResource) { + String key = getPresenceMapKey(userWithResource); + String resource = StringUtils.parseResource(userWithResource); + Map<String, Presence> userPresences = presenceMap.get(key); + if (userPresences == null) { + Presence presence = new Presence(Presence.Type.unavailable); + presence.setFrom(userWithResource); + return presence; + } + else { + Presence presence = userPresences.get(resource); + if (presence == null) { + presence = new Presence(Presence.Type.unavailable); + presence.setFrom(userWithResource); + return presence; + } + else { + return presence; + } + } + } + + /** + * Returns an iterator (of Presence objects) for all of a user's current presences + * or an unavailable presence if the user is unavailable (offline) or if no presence + * information is available, such as when you are not subscribed to the user's presence + * updates. + * + * @param user a XMPP ID, e.g. jdoe@example.com. + * @return an iterator (of Presence objects) for all the user's current presences, + * or an unavailable presence if the user is offline or if no presence information + * is available. + */ + public Iterator<Presence> getPresences(String user) { + String key = getPresenceMapKey(user); + Map<String, Presence> userPresences = presenceMap.get(key); + if (userPresences == null) { + Presence presence = new Presence(Presence.Type.unavailable); + presence.setFrom(user); + return Arrays.asList(presence).iterator(); + } + else { + Collection<Presence> answer = new ArrayList<Presence>(); + for (Presence presence : userPresences.values()) { + if (presence.isAvailable()) { + answer.add(presence); + } + } + if (!answer.isEmpty()) { + return answer.iterator(); + } + else { + Presence presence = new Presence(Presence.Type.unavailable); + presence.setFrom(user); + return Arrays.asList(presence).iterator(); + } + } + } + + /** + * Cleans up all resources used by the roster. + */ + void cleanup() { + rosterListeners.clear(); + } + + /** + * Returns the key to use in the presenceMap for a fully qualified XMPP ID. + * The roster can contain any valid address format such us "domain/resource", + * "user@domain" or "user@domain/resource". If the roster contains an entry + * associated with the fully qualified XMPP ID then use the fully qualified XMPP + * ID as the key in presenceMap, otherwise use the bare address. Note: When the + * key in presenceMap is a fully qualified XMPP ID, the userPresences is useless + * since it will always contain one entry for the user. + * + * @param user the bare or fully qualified XMPP ID, e.g. jdoe@example.com or + * jdoe@example.com/Work. + * @return the key to use in the presenceMap for the fully qualified XMPP ID. + */ + private String getPresenceMapKey(String user) { + if (user == null) { + return null; + } + String key = user; + if (!contains(user)) { + key = StringUtils.parseBareAddress(user); + } + return key.toLowerCase(); + } + + /** + * Changes the presence of available contacts offline by simulating an unavailable + * presence sent from the server. After a disconnection, every Presence is set + * to offline. + */ + private void setOfflinePresences() { + Presence packetUnavailable; + for (String user : presenceMap.keySet()) { + Map<String, Presence> resources = presenceMap.get(user); + if (resources != null) { + for (String resource : resources.keySet()) { + packetUnavailable = new Presence(Presence.Type.unavailable); + packetUnavailable.setFrom(user + "/" + resource); + presencePacketListener.processPacket(packetUnavailable); + } + } + } + } + + /** + * Fires roster changed event to roster listeners indicating that the + * specified collections of contacts have been added, updated or deleted + * from the roster. + * + * @param addedEntries the collection of address of the added contacts. + * @param updatedEntries the collection of address of the updated contacts. + * @param deletedEntries the collection of address of the deleted contacts. + */ + private void fireRosterChangedEvent(Collection<String> addedEntries, Collection<String> updatedEntries, + Collection<String> deletedEntries) { + for (RosterListener listener : rosterListeners) { + if (!addedEntries.isEmpty()) { + listener.entriesAdded(addedEntries); + } + if (!updatedEntries.isEmpty()) { + listener.entriesUpdated(updatedEntries); + } + if (!deletedEntries.isEmpty()) { + listener.entriesDeleted(deletedEntries); + } + } + } + + /** + * Fires roster presence changed event to roster listeners. + * + * @param presence the presence change. + */ + private void fireRosterPresenceEvent(Presence presence) { + for (RosterListener listener : rosterListeners) { + listener.presenceChanged(presence); + } + } + + /** + * An enumeration for the subscription mode options. + */ + public enum SubscriptionMode { + + /** + * Automatically accept all subscription and unsubscription requests. This is + * the default mode and is suitable for simple client. More complex client will + * likely wish to handle subscription requests manually. + */ + accept_all, + + /** + * Automatically reject all subscription requests. + */ + reject_all, + + /** + * Subscription requests are ignored, which means they must be manually + * processed by registering a listener for presence packets and then looking + * for any presence requests that have the type Presence.Type.SUBSCRIBE or + * Presence.Type.UNSUBSCRIBE. + */ + manual + } + + /** + * Listens for all presence packets and processes them. + */ + private class PresencePacketListener implements PacketListener { + + public void processPacket(Packet packet) { + Presence presence = (Presence) packet; + String from = presence.getFrom(); + String key = getPresenceMapKey(from); + + // If an "available" presence, add it to the presence map. Each presence + // map will hold for a particular user a map with the presence + // packets saved for each resource. + if (presence.getType() == Presence.Type.available) { + Map<String, Presence> userPresences; + // Get the user presence map + if (presenceMap.get(key) == null) { + userPresences = new ConcurrentHashMap<String, Presence>(); + presenceMap.put(key, userPresences); + } + else { + userPresences = presenceMap.get(key); + } + // See if an offline presence was being stored in the map. If so, remove + // it since we now have an online presence. + userPresences.remove(""); + // Add the new presence, using the resources as a key. + userPresences.put(StringUtils.parseResource(from), presence); + // If the user is in the roster, fire an event. + RosterEntry entry = entries.get(key); + if (entry != null) { + fireRosterPresenceEvent(presence); + } + } + // If an "unavailable" packet. + else if (presence.getType() == Presence.Type.unavailable) { + // If no resource, this is likely an offline presence as part of + // a roster presence flood. In that case, we store it. + if ("".equals(StringUtils.parseResource(from))) { + Map<String, Presence> userPresences; + // Get the user presence map + if (presenceMap.get(key) == null) { + userPresences = new ConcurrentHashMap<String, Presence>(); + presenceMap.put(key, userPresences); + } + else { + userPresences = presenceMap.get(key); + } + userPresences.put("", presence); + } + // Otherwise, this is a normal offline presence. + else if (presenceMap.get(key) != null) { + Map<String, Presence> userPresences = presenceMap.get(key); + // Store the offline presence, as it may include extra information + // such as the user being on vacation. + userPresences.put(StringUtils.parseResource(from), presence); + } + // If the user is in the roster, fire an event. + RosterEntry entry = entries.get(key); + if (entry != null) { + fireRosterPresenceEvent(presence); + } + } + else if (presence.getType() == Presence.Type.subscribe) { + if (subscriptionMode == SubscriptionMode.accept_all) { + // Accept all subscription requests. + Presence response = new Presence(Presence.Type.subscribed); + response.setTo(presence.getFrom()); + connection.sendPacket(response); + } + else if (subscriptionMode == SubscriptionMode.reject_all) { + // Reject all subscription requests. + Presence response = new Presence(Presence.Type.unsubscribed); + response.setTo(presence.getFrom()); + connection.sendPacket(response); + } + // Otherwise, in manual mode so ignore. + } + else if (presence.getType() == Presence.Type.unsubscribe) { + if (subscriptionMode != SubscriptionMode.manual) { + // Acknowledge and accept unsubscription notification so that the + // server will stop sending notifications saying that the contact + // has unsubscribed to our presence. + Presence response = new Presence(Presence.Type.unsubscribed); + response.setTo(presence.getFrom()); + connection.sendPacket(response); + } + // Otherwise, in manual mode so ignore. + } + // Error presence packets from a bare JID mean we invalidate all existing + // presence info for the user. + else if (presence.getType() == Presence.Type.error && + "".equals(StringUtils.parseResource(from))) + { + Map<String, Presence> userPresences; + if (!presenceMap.containsKey(key)) { + userPresences = new ConcurrentHashMap<String, Presence>(); + presenceMap.put(key, userPresences); + } + else { + userPresences = presenceMap.get(key); + // Any other presence data is invalidated by the error packet. + userPresences.clear(); + } + // Set the new presence using the empty resource as a key. + userPresences.put("", presence); + // If the user is in the roster, fire an event. + RosterEntry entry = entries.get(key); + if (entry != null) { + fireRosterPresenceEvent(presence); + } + } + } + } + + /** + * Listen for empty IQ results which indicate that the client has already a current + * roster version + * @author Till Klocke + * + */ + + private class RosterResultListener implements PacketListener{ + + public void processPacket(Packet packet) { + if(packet instanceof IQ){ + IQ result = (IQ)packet; + if(result.getType().equals(IQ.Type.RESULT) && result.getExtensions().isEmpty()){ + Collection<String> addedEntries = new ArrayList<String>(); + Collection<String> updatedEntries = new ArrayList<String>(); + Collection<String> deletedEntries = new ArrayList<String>(); + if(persistentStorage!=null){ + for(RosterPacket.Item item : persistentStorage.getEntries()){ + insertRosterItem(item,addedEntries,updatedEntries,deletedEntries); + } + } + synchronized (Roster.this) { + rosterInitialized = true; + Roster.this.notifyAll(); + } + fireRosterChangedEvent(addedEntries,updatedEntries,deletedEntries); + } + } + connection.removePacketListener(this); + } + } + + /** + * Listens for all roster packets and processes them. + */ + private class RosterPacketListener implements PacketListener { + + public void processPacket(Packet packet) { + // Keep a registry of the entries that were added, deleted or updated. An event + // will be fired for each affected entry + Collection<String> addedEntries = new ArrayList<String>(); + Collection<String> updatedEntries = new ArrayList<String>(); + Collection<String> deletedEntries = new ArrayList<String>(); + + String version=null; + RosterPacket rosterPacket = (RosterPacket) packet; + List<RosterPacket.Item> rosterItems = new ArrayList<RosterPacket.Item>(); + for(RosterPacket.Item item : rosterPacket.getRosterItems()){ + rosterItems.add(item); + } + //Here we check if the server send a versioned roster, if not we do not use + //the roster storage to store entries and work like in the old times + if(rosterPacket.getVersion()==null){ + persistentStorage=null; + } else{ + version = rosterPacket.getVersion(); + } + + if(persistentStorage!=null && !rosterInitialized){ + for(RosterPacket.Item item : persistentStorage.getEntries()){ + rosterItems.add(item); + } + } + + for (RosterPacket.Item item : rosterItems) { + insertRosterItem(item,addedEntries,updatedEntries,deletedEntries); + } + if(persistentStorage!=null){ + for (RosterPacket.Item i : rosterPacket.getRosterItems()){ + if(i.getItemType().equals(RosterPacket.ItemType.remove)){ + persistentStorage.removeEntry(i.getUser()); + } + else{ + persistentStorage.addEntry(i, version); + } + } + } + // Mark the roster as initialized. + synchronized (Roster.this) { + rosterInitialized = true; + Roster.this.notifyAll(); + } + + // Fire event for roster listeners. + fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); + } + } +} |