/** * $RCSfile$ * $Revision$ * $Date$ * * Copyright 2003-2004 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.packet.*; import org.jivesoftware.smack.filter.*; import org.jivesoftware.smack.util.StringUtils; import java.util.*; /** * 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.

* * Others users may attempt to subscribe to this user using a subscription request. Three * modes are supported for handling these requests:

* * @see XMPPConnection#getRoster() * @author Matt Tucker */ public class Roster { /** * 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. */ public static final int SUBSCRIPTION_ACCEPT_ALL = 0; /** * Automatically reject all subscription requests. */ public static final int SUBSCRIPTION_REJECT_ALL = 1; /** * 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. */ public static final int SUBSCRIPTION_MANUAL = 2; /** * The default subscription processing mode to use when a Roster is created. By default * all subscription requests are automatically accepted. */ private static int defaultSubscriptionMode = SUBSCRIPTION_ACCEPT_ALL; private XMPPConnection connection; private Map groups; private List entries; private List unfiledEntries; private List rosterListeners; private Map presenceMap; // The roster is marked as initialized when at least a single roster packet // has been recieved and processed. boolean rosterInitialized = false; private int subscriptionMode = getDefaultSubscriptionMode(); /** * 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 #SUBSCRIPTION_ACCEPT_ALL}. * * @return the default subscription mode to use for new Rosters */ public static int 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 #SUBSCRIPTION_ACCEPT_ALL}. * * @param subscriptionMode the default subscription mode to use for new Rosters. */ public static void setDefaultSubscriptionMode(int subscriptionMode) { defaultSubscriptionMode = subscriptionMode; } /** * Creates a new roster. * * @param connection an XMPP connection. */ Roster(final XMPPConnection connection) { this.connection = connection; groups = new Hashtable(); unfiledEntries = new ArrayList(); entries = new ArrayList(); rosterListeners = new ArrayList(); presenceMap = new HashMap(); // 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); connection.addPacketListener(new PresencePacketListener(), presenceFilter); } /** * 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 #SUBSCRIPTION_ACCEPT_ALL}.

* * 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 int 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 #SUBSCRIPTION_ACCEPT_ALL}.

* * 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(int subscriptionMode) { if (subscriptionMode != SUBSCRIPTION_ACCEPT_ALL && subscriptionMode != SUBSCRIPTION_REJECT_ALL && subscriptionMode != SUBSCRIPTION_MANUAL) { throw new IllegalArgumentException("Invalid mode."); } 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. */ public void reload() { connection.sendPacket(new RosterPacket()); } /** * 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) { synchronized (rosterListeners) { 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) { synchronized (rosterListeners) { rosterListeners.remove(rosterListener); } } /** * Creates a new group.

* * 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. */ public RosterGroup createGroup(String name) { synchronized (groups) { 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 null if the * the roster entry won't belong to a group. */ public void createEntry(String user, String name, String [] groups) throws XMPPException { // 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 (int i=0; i>Packet(Packet) synchronized (entries) { if (!entries.contains(entry)) { 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()); } else { } } /** * Returns a count of the entries in the roster. * * @return the number of entries in the roster. */ public int getEntryCount() { HashMap entryMap = new HashMap(); // Loop through all roster groups. for (Iterator groups = getGroups(); groups.hasNext(); ) { RosterGroup rosterGroup = (RosterGroup) groups.next(); for (Iterator entries = rosterGroup.getEntries(); entries.hasNext(); ) { entryMap.put(entries.next(), ""); } } synchronized (unfiledEntries) { return entryMap.size() + unfiledEntries.size(); } } /** * Returns all entries in the roster, including entries that don't belong to * any groups. * * @return all entries in the roster. */ public Iterator getEntries() { ArrayList allEntries = new ArrayList(); // Loop through all roster groups and add their entries to the answer for (Iterator groups = getGroups(); groups.hasNext(); ) { RosterGroup rosterGroup = (RosterGroup) groups.next(); for (Iterator entries = rosterGroup.getEntries(); entries.hasNext(); ) { RosterEntry entry = (RosterEntry)entries.next(); if (!allEntries.contains(entry)) { allEntries.add(entry); } } } // Add the roster unfiled entries to the answer synchronized (unfiledEntries) { allEntries.addAll(unfiledEntries); } return allEntries.iterator(); } /** * 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() { synchronized (unfiledEntries) { return unfiledEntries.size(); } } /** * Returns an Iterator for the unfiled roster entries. An unfiled entry is * an entry that doesn't belong to any groups. * * @return an iterator the unfiled roster entries. */ public Iterator getUnfiledEntries() { synchronized (unfiledEntries) { return Collections.unmodifiableList(new ArrayList(unfiledEntries)).iterator(); } } /** * Returns the roster entry associated with the given XMPP address or * null 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 null if it does not exist. */ public RosterEntry getEntry(String user) { if (user == null) { return null; } synchronized (entries) { for (Iterator i=entries.iterator(); i.hasNext(); ) { RosterEntry entry = (RosterEntry)i.next(); if (entry.getUser().toLowerCase().equals(user.toLowerCase())) { return entry; } } } return null; } /** * 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) { if (user == null) { return false; } synchronized (entries) { for (Iterator i=entries.iterator(); i.hasNext(); ) { RosterEntry entry = (RosterEntry)i.next(); if (entry.getUser().toLowerCase().equals(user.toLowerCase())) { return true; } } } return false; } /** * Returns the roster group with the specified name, or null 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) { synchronized (groups) { return (RosterGroup)groups.get(name); } } /** * Returns the number of the groups in the roster. * * @return the number of groups in the roster. */ public int getGroupCount() { synchronized (groups) { return groups.size(); } } /** * Returns an iterator the for all the roster groups. * * @return an iterator for all roster groups. */ public Iterator getGroups() { synchronized (groups) { List groupsList = Collections.unmodifiableList(new ArrayList(groups.values())); return groupsList.iterator(); } } /** * Returns the presence info for a particular user, or null 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.

* * If the user has several presences (one for each resource) then answer the presence * with the highest priority. * * @param user a fully qualified xmpp ID. The address could be in any valid format (e.g. * "domain/resource", "user@domain" or "user@domain/resource"). * @return the user's current presence, or null if the user is unavailable * or if no presence information is available.. */ public Presence getPresence(String user) { String key = getPresenceMapKey(user); Map userPresences = (Map) presenceMap.get(key); if (userPresences == null) { return null; } else { // Find the resource with the highest priority // Might be changed to use the resource with the highest availability instead. Iterator it = userPresences.keySet().iterator(); Presence p; Presence presence = null; while (it.hasNext()) { p = (Presence) userPresences.get(it.next()); if (presence == null) { presence = p; } else { if (p.getPriority() > presence.getPriority()) { presence = p; } } } return presence; } } /** * Returns the presence info for a particular user's resource, or null 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 userResource a fully qualified xmpp ID including a resource. * @return the user's current presence, or null if the user is unavailable * or if no presence information is available. */ public Presence getPresenceResource(String userResource) { String key = getPresenceMapKey(userResource); String resource = StringUtils.parseResource(userResource); Map userPresences = (Map)presenceMap.get(key); if (userPresences == null) { return null; } else { return (Presence) userPresences.get(resource); } } /** * Returns an iterator (of Presence objects) for all the user's current presences * or null 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 fully qualified xmpp ID, e.g. jdoe@example.com * @return an iterator (of Presence objects) for all the user's current presences, * or null if the user is unavailable or if no presence information * is available. */ public Iterator getPresences(String user) { String key = getPresenceMapKey(user); Map userPresences = (Map)presenceMap.get(key); if (userPresences == null) { return null; } else { synchronized (userPresences) { return new HashMap(userPresences).values().iterator(); } } } /** * 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 fully qualified xmpp ID, e.g. jdoe@example.com/Work. * @return the key to use in the presenceMap for the fully qualified xmpp ID. */ private String getPresenceMapKey(String user) { String key = user; if (!contains(user)) { key = StringUtils.parseBareAddress(user); } return key; } /** * 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 addedEntries, Collection updatedEntries, Collection deletedEntries) { RosterListener [] listeners = null; synchronized (rosterListeners) { listeners = new RosterListener[rosterListeners.size()]; rosterListeners.toArray(listeners); } for (int i=0; i