/** * $RCSfile$ * $Revision$ * $Date$ * * Copyright (C) 2002-2003 Jive Software. All rights reserved. * ==================================================================== * The Jive Software License (based on Apache Software License, Version 1.1) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, * if any, must include the following acknowledgment: * "This product includes software developed by * Jive Software (http://www.jivesoftware.com)." * Alternately, this acknowledgment may appear in the software itself, * if and wherever such third-party acknowledgments normally appear. * * 4. The names "Smack" and "Jive Software" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please * contact webmaster@jivesoftware.com. * * 5. Products derived from this software may not be called "Smack", * nor may "Smack" appear in their name, without prior written * permission of Jive Software. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== */ 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:

* * All presence subscription requests are automatically approved to this client * are automatically approved. This logic will be updated in the future to allow for * pluggable behavior. * * @see XMPPConnection#getRoster() * @author Matt Tucker */ public class Roster { /** * Automatically accept all subscription 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. */ 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 defaulSubscriptionMode = 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 = getDefaulSubscriptionMode(); /** * 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 getDefaulSubscriptionMode() { return defaulSubscriptionMode; } /** * 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 setDefaulSubscriptionMode(int subscriptionMode) { defaulSubscriptionMode = 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 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 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; } } /** * Cretaes a new roster entry and prsence subscription. The server will asynchronously * update the roster with the subscription status. * * @param user the user. * @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)) { 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); connection.sendPacket(packet); } } } /** * 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"). * @return the roster entry or null if it does not exist. */ public RosterEntry getEntry(String user) { if (user == null) { return null; } // Roster entries never include a resource so remove the resource // if it's a part of the XMPP address. user = StringUtils.parseBareAddress(user); synchronized (entries) { for (Iterator i=entries.iterator(); i.hasNext(); ) { RosterEntry entry = (RosterEntry)i.next(); if (entry.getUser().equals(user)) { 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"). * @return true if the XMPP address is an entry in the roster. */ public boolean contains(String user) { if (user == null) { return false; } // Roster entries never include a resource so remove the resource // if it's a part of the XMPP address. user = StringUtils.parseBareAddress(user); synchronized (entries) { for (Iterator i=entries.iterator(); i.hasNext(); ) { RosterEntry entry = (RosterEntry)i.next(); if (entry.getUser().equals(user)) { 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, e.g. jdoe@example.com * @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 = StringUtils.parseName(user) + "@" + StringUtils.parseServer(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 user a fully qualified xmpp ID including a resource, e.g. jdoe@example.com/Home * @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 = StringUtils.parseName(userResource) + "@" + StringUtils.parseServer(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 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 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 = StringUtils.parseName(user) + "@" + StringUtils.parseServer(user); Map userPresences = (Map) presenceMap.get(key); if (userPresences == null) { return null; } else { return userPresences.values().iterator(); } } /** * Fires roster changed event to roster listeners. */ private void fireRosterChangedEvent() { RosterListener [] listeners = null; synchronized (rosterListeners) { listeners = new RosterListener[rosterListeners.size()]; rosterListeners.toArray(listeners); } for (int i=0; i