1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2024-06-23 20:04:51 +02:00
Smack/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java
Florian Schmaus 4698805a34 Rename 'Packet' class to 'Stanza'
Smack still uses the term 'Packet' in some places. This is just the
first step towards using correct XMPP terms in Smack.
2015-02-06 09:34:51 +01:00

1380 lines
55 KiB
Java

/**
*
* Copyright 2003-2007 Jive Software.
*
* 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.roster;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.AbstractConnectionClosedListener;
import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.ExceptionCallback;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.SmackException.NotLoggedInException;
import org.jivesoftware.smack.XMPPConnectionRegistry;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.packet.XMPPError.Condition;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jivesoftware.smack.roster.packet.RosterVer;
import org.jivesoftware.smack.roster.packet.RosterPacket.Item;
import org.jivesoftware.smack.roster.rosterstore.RosterStore;
import org.jivesoftware.smack.util.Objects;
import org.jxmpp.util.XmppStringUtils;
/**
* 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 #getInstanceFor(XMPPConnection)
*/
public class Roster extends Manager {
private static final Logger LOGGER = Logger.getLogger(Roster.class.getName());
static {
XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
@Override
public void connectionCreated(XMPPConnection connection) {
getInstanceFor(connection);
}
});
}
private static final Map<XMPPConnection, Roster> INSTANCES = new WeakHashMap<>();
/**
* Returns the roster for the user.
* <p>
* This method will never return <code>null</code>, instead if the user has not yet logged into
* the server or is logged in anonymously all modifying methods of the returned roster object
* like {@link Roster#createEntry(String, String, String[])},
* {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing
* {@link RosterListener}s will throw an IllegalStateException.
*
* @return the user's roster.
* @throws IllegalStateException if the connection is anonymous
*/
public static synchronized Roster getInstanceFor(XMPPConnection connection) {
Roster roster = INSTANCES.get(connection);
if (roster == null) {
roster = new Roster(connection);
INSTANCES.put(connection, roster);
}
return roster;
}
private static final PacketFilter PRESENCE_PACKET_FILTER = new PacketTypeFilter(Presence.class);
private static boolean rosterLoadedAtLoginDefault = true;
/**
* 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 RosterStore rosterStore;
private final Map<String, RosterGroup> groups = new ConcurrentHashMap<String, RosterGroup>();
/**
* Concurrent hash map from JID to its roster entry.
*/
private final Map<String,RosterEntry> entries = new ConcurrentHashMap<String,RosterEntry>();
private final Set<RosterEntry> unfiledEntries = new CopyOnWriteArraySet<>();
private final Set<RosterListener> rosterListeners = new LinkedHashSet<>();
private final Map<String, Map<String, Presence>> presenceMap = new ConcurrentHashMap<String, Map<String, Presence>>();
/**
* Mutually exclude roster listener invocation and changing the {@link entries} map. Also used
* to synchronize access to either the roster listeners or the entries map.
*/
private final Object rosterListenersAndEntriesLock = new Object();
// The roster is marked as initialized when at least a single roster packet
// has been received and processed.
private boolean loaded = false;
private final PresencePacketListener presencePacketListener = new PresencePacketListener();
/**
*
*/
private boolean rosterLoadedAtLogin = rosterLoadedAtLoginDefault;
private SubscriptionMode 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 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;
}
/**
* Creates a new roster.
*
* @param connection an XMPP connection.
*/
private Roster(final XMPPConnection connection) {
super(connection);
// Note that we use sync packet listeners because RosterListeners should be invoked in the same order as the
// roster stanzas arrive.
// Listen for any roster packets.
connection.registerIQRequestHandler(new RosterPushListener());
// Listen for any presence packets.
connection.addSyncPacketListener(presencePacketListener, PRESENCE_PACKET_FILTER);
// Listen for connection events
connection.addConnectionListener(new AbstractConnectionClosedListener() {
@Override
public void authenticated(XMPPConnection connection, boolean resumed) {
// Anonymous users can't have a roster, but it is possible that a Roster instance is
// retrieved if getRoster() is called *before* connect(). So we have to check here
// again if it's an anonymous connection.
if (connection.isAnonymous())
return;
if (!isRosterLoadedAtLogin())
return;
// We are done here if the connection was resumed
if (resumed) {
return;
}
try {
Roster.this.reload();
}
catch (SmackException e) {
LOGGER.log(Level.SEVERE, "Could not reload Roster", e);
return;
}
}
@Override
public void connectionTerminated() {
// Changes the presence available contacts to unavailable
setOfflinePresencesAndResetLoaded();
}
});
// If the connection is already established, call reload
if (connection.isAuthenticated()) {
try {
reload();
}
catch (SmackException e) {
LOGGER.log(Level.SEVERE, "Could not reload Roster", e);
}
}
}
/**
* 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 NotLoggedInException If not logged in.
* @throws NotConnectedException
*/
public void reload() throws NotLoggedInException, NotConnectedException{
final XMPPConnection connection = connection();
if (!connection.isAuthenticated()) {
throw new NotLoggedInException();
}
if (connection.isAnonymous()) {
throw new IllegalStateException("Anonymous users can't have a roster.");
}
RosterPacket packet = new RosterPacket();
if (rosterStore != null && isRosterVersioningSupported()) {
packet.setVersion(rosterStore.getRosterVersion());
}
connection.sendIqWithResponseCallback(packet, new RosterResultListener(), new ExceptionCallback() {
@Override
public void processException(Exception exception) {
LOGGER.log(Level.SEVERE, "Exception reloading roster" , exception);
}
});
}
/**
* Reload the roster and block until it is reloaded.
*
* @throws NotLoggedInException
* @throws NotConnectedException
* @since 4.1
*/
public void reloadAndWait() throws NotLoggedInException, NotConnectedException {
reload();
waitUntilLoaded();
}
/**
* Set the roster store, may cause a roster reload
*
* @param rosterStore
* @return true if the roster reload was initiated, false otherwise.
* @since 4.1
*/
public boolean setRosterStore(RosterStore rosterStore) {
this.rosterStore = rosterStore;
try {
reload();
}
catch (NotLoggedInException | NotConnectedException e) {
LOGGER.log(Level.FINER, "Could not reload roster", e);
return false;
}
return true;
}
protected boolean waitUntilLoaded() {
final XMPPConnection connection = connection();
while (!loaded) {
long waitTime = connection.getPacketReplyTimeout();
long start = System.currentTimeMillis();
if (waitTime <= 0) {
break;
}
try {
synchronized (this) {
if (!loaded) {
wait(waitTime);
}
}
}
catch (InterruptedException e) {
LOGGER.log(Level.FINE, "interrupted", e);
break;
}
long now = System.currentTimeMillis();
waitTime -= now - start;
start = now;
}
return isLoaded();
}
/**
* Check if the roster is loaded.
*
* @return true if the roster is loaded.
* @since 4.1
*/
public boolean isLoaded() {
return loaded;
}
/**
* 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.
* @return true if the listener was not already added.
* @see #getEntriesAndAddListener(RosterListener, RosterEntries)
*/
public boolean addRosterListener(RosterListener rosterListener) {
synchronized (rosterListenersAndEntriesLock) {
return 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.
* @return true if the listener was active and got removed.
*/
public boolean removeRosterListener(RosterListener rosterListener) {
synchronized (rosterListenersAndEntriesLock) {
return 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, or null if the group already exists
* @throws IllegalStateException if logged in anonymously
*/
public RosterGroup createGroup(String name) {
final XMPPConnection connection = connection();
if (connection.isAnonymous()) {
throw new IllegalStateException("Anonymous users can't have a roster.");
}
if (groups.containsKey(name)) {
return groups.get(name);
}
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 NoResponseException if there was no response from the server.
* @throws XMPPErrorException if an XMPP exception occurs.
* @throws NotLoggedInException If not logged in.
* @throws NotConnectedException
*/
public void createEntry(String user, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException {
final XMPPConnection connection = connection();
if (!connection.isAuthenticated()) {
throw new NotLoggedInException();
}
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);
connection.createPacketCollectorAndSend(rosterPacket).nextResultOrThrow();
// Create a presence subscription packet and send.
Presence presencePacket = new Presence(Presence.Type.subscribe);
presencePacket.setTo(user);
connection.sendPacket(presencePacket);
}
/**
* 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 a synchronous call -- Smack must wait for the server
* to send an updated subscription status.
*
* @param entry a roster entry.
* @throws XMPPErrorException if an XMPP error occurs.
* @throws NotLoggedInException if not logged in.
* @throws NoResponseException SmackException if there was no response from the server.
* @throws NotConnectedException
* @throws IllegalStateException if connection is not logged in or logged in anonymously
*/
public void removeEntry(RosterEntry entry) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException {
final XMPPConnection connection = connection();
if (!connection.isAuthenticated()) {
throw new NotLoggedInException();
}
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);
connection.createPacketCollectorAndSend(packet).nextResultOrThrow();
}
/**
* Returns a count of the entries in the roster.
*
* @return the number of entries in the roster.
*/
public int getEntryCount() {
return getEntries().size();
}
/**
* Add a roster listener and invoke the roster entries with all entries of the roster.
* <p>
* The method guarantees that the listener is only invoked after
* {@link RosterEntries#rosterEntires(Collection)} has been invoked, and that all roster events
* that happen while <code>rosterEntires(Collection) </code> is called are queued until the
* method returns.
* </p>
* <p>
* This guarantee makes this the ideal method to e.g. populate a UI element with the roster while
* installing a {@link RosterListener} to listen for subsequent roster events.
* </p>
*
* @param rosterListener the listener to install
* @param rosterEntries the roster entries callback interface
* @since 4.1
*/
public void getEntriesAndAddListener(RosterListener rosterListener, RosterEntries rosterEntries) {
Objects.requireNonNull(rosterListener, "listener must not be null");
Objects.requireNonNull(rosterEntries, "rosterEntries must not be null");
synchronized (rosterListenersAndEntriesLock) {
rosterEntries.rosterEntires(entries.values());
addRosterListener(rosterListener);
}
}
/**
* Returns a set of all entries in the roster, including entries
* that don't belong to any groups.
*
* @return all entries in the roster.
*/
public Set<RosterEntry> getEntries() {
Set<RosterEntry> allEntries;
synchronized (rosterListenersAndEntriesLock) {
allEntries = new HashSet<>(entries.size());
for (RosterEntry entry : entries.values()) {
allEntries.add(entry);
}
}
return 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 set for the unfiled roster entries. An unfiled entry is
* an entry that doesn't belong to any groups.
*
* @return the unfiled roster entries.
*/
public Set<RosterEntry> getUnfiledEntries() {
return Collections.unmodifiableSet(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;
}
String key = getMapKey(user);
return entries.get(key);
}
/**
* 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 = getMapKey(XmppStringUtils.parseBareJid(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;
// This is used in case no available presence is found
Presence unavailable = null;
for (String resource : userPresences.keySet()) {
Presence p = userPresences.get(resource);
if (!p.isAvailable()) {
unavailable = p;
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) {
if (unavailable != null) {
return unavailable;
}
else {
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 = getMapKey(userWithResource);
String resource = XmppStringUtils.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 a List of Presence objects for all of a user's current presences if no presence information is available,
* such as when you are not subscribed to the user's presence updates.
*
* @param bareJid a XMPP ID, e.g. jdoe@example.com.
* @return a List of Presence objects for all the user's current presences, or an unavailable presence if no
* presence information is available.
*/
public List<Presence> getAllPresences(String bareJid) {
Map<String, Presence> userPresences = presenceMap.get(getMapKey(bareJid));
List<Presence> res;
if (userPresences == null) {
// Create an unavailable presence if none was found
Presence unavailable = new Presence(Presence.Type.unavailable);
unavailable.setFrom(bareJid);
res = new ArrayList<>(Arrays.asList(unavailable));
} else {
res = new ArrayList<>(userPresences.values());
}
return res;
}
/**
* Returns a List of all <b>available</b> Presence Objects for the given bare JID. If there are no available
* presences, then the empty list will be returned.
*
* @param bareJid the bare JID from which the presences should be retrieved.
* @return available presences for the bare JID.
*/
public List<Presence> getAvailablePresences(String bareJid) {
List<Presence> allPresences = getAllPresences(bareJid);
List<Presence> res = new ArrayList<>(allPresences.size());
for (Presence presence : allPresences) {
if (presence.isAvailable()) {
res.add(presence);
}
}
return res;
}
/**
* Returns a List 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 a List 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 List<Presence> getPresences(String user) {
List<Presence> res;
String key = getMapKey(user);
Map<String, Presence> userPresences = presenceMap.get(key);
if (userPresences == null) {
Presence presence = new Presence(Presence.Type.unavailable);
presence.setFrom(user);
res = Arrays.asList(presence);
}
else {
List<Presence> answer = new ArrayList<Presence>();
// Used in case no available presence is found
Presence unavailable = null;
for (Presence presence : userPresences.values()) {
if (presence.isAvailable()) {
answer.add(presence);
}
else {
unavailable = presence;
}
}
if (!answer.isEmpty()) {
res = answer;
}
else if (unavailable != null) {
res = Arrays.asList(unavailable);
}
else {
Presence presence = new Presence(Presence.Type.unavailable);
presence.setFrom(user);
res = Arrays.asList(presence);
}
}
return Collections.unmodifiableList(res);
}
/**
* Check if the given JID is subscribed to the user's presence.
* <p>
* If the JID is subscribed to the user's presence then it is allowed to see the presence and
* will get notified about presence changes. Also returns true, if the JID is the service
* name of the XMPP connection (the "XMPP domain"), i.e. the XMPP service is treated like
* having an implicit subscription to the users presence.
* </p>
* Note that if the roster is not loaded, then this method will always return false.
*
* @param jid
* @return true if the given JID is allowed to see the users presence.
* @since 4.1
*/
public boolean isSubscribedToMyPresence(String jid) {
if (connection().getServiceName().equals(jid)) {
return true;
}
RosterEntry entry = getEntry(jid);
if (entry == null) {
return false;
}
switch (entry.getType()) {
case from:
case both:
return true;
default:
return false;
}
}
/**
* Sets if the roster will be loaded from the server when logging in. This
* is the common behaviour for clients but sometimes clients may want to differ this
* or just never do it if not interested in rosters.
*
* @param rosterLoadedAtLogin if the roster will be loaded from the server when logging in.
*/
public void setRosterLoadedAtLogin(boolean rosterLoadedAtLogin) {
this.rosterLoadedAtLogin = rosterLoadedAtLogin;
}
/**
* Returns true if the roster will be loaded from the server when logging in. This
* is the common behavior for clients but sometimes clients may want to differ this
* or just never do it if not interested in rosters.
*
* @return true if the roster will be loaded from the server when logging in.
* @see <a href="http://xmpp.org/rfcs/rfc6121.html#roster-login">RFC 6121 2.2 - Retrieving the Roster on Login</a>
*/
public boolean isRosterLoadedAtLogin() {
return rosterLoadedAtLogin;
}
RosterStore getRosterStore() {
return rosterStore;
}
/**
* Returns the key to use in the presenceMap and entries Map 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 and entries Map for the fully qualified XMPP ID.
*/
private String getMapKey(String user) {
if (user == null) {
return null;
}
if (entries.containsKey(user)) {
return user;
}
String key = XmppStringUtils.parseBareJid(user);
return key.toLowerCase(Locale.US);
}
/**
* 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.
* @throws NotConnectedException
*/
private void setOfflinePresencesAndResetLoaded() {
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);
try {
presencePacketListener.processPacket(packetUnavailable);
}
catch (NotConnectedException e) {
throw new IllegalStateException(
"presencePakcetListener should never throw a NotConnectedException when processPacket is called with a presence of type unavailable",
e);
}
}
}
}
loaded = false;
}
/**
* 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(final Collection<String> addedEntries, final Collection<String> updatedEntries,
final Collection<String> deletedEntries) {
synchronized (rosterListenersAndEntriesLock) {
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(final Presence presence) {
synchronized (rosterListenersAndEntriesLock) {
for (RosterListener listener : rosterListeners) {
listener.presenceChanged(presence);
}
}
}
private void addUpdateEntry(Collection<String> addedEntries, Collection<String> updatedEntries,
Collection<String> unchangedEntries, RosterPacket.Item item, RosterEntry entry) {
RosterEntry oldEntry;
synchronized (rosterListenersAndEntriesLock) {
oldEntry = entries.put(item.getUser(), entry);
}
if (oldEntry == null) {
addedEntries.add(item.getUser());
}
else {
RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry);
if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) {
updatedEntries.add(item.getUser());
} else {
// Record the entry as unchanged, so that it doesn't end up as deleted entry
unchangedEntries.add(item.getUser());
}
}
// Mark the entry as unfiled if it does not belong to any groups.
if (item.getGroupNames().isEmpty()) {
unfiledEntries.add(entry);
}
else {
unfiledEntries.remove(entry);
}
// Add the entry/user to the groups
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);
}
// Remove user from the remaining groups.
List<String> oldGroupNames = new ArrayList<String>();
for (RosterGroup group: getGroups()) {
oldGroupNames.add(group.getName());
}
oldGroupNames.removeAll(newGroupNames);
for (String groupName : oldGroupNames) {
RosterGroup group = getGroup(groupName);
group.removeEntryLocal(entry);
if (group.getEntryCount() == 0) {
groups.remove(groupName);
}
}
}
private void deleteEntry(Collection<String> deletedEntries, RosterEntry entry) {
String user = entry.getUser();
entries.remove(user);
unfiledEntries.remove(entry);
presenceMap.remove(XmppStringUtils.parseBareJid(user));
deletedEntries.add(user);
for (Entry<String,RosterGroup> e: groups.entrySet()) {
RosterGroup group = e.getValue();
group.removeEntryLocal(entry);
if (group.getEntryCount() == 0) {
groups.remove(e.getKey());
}
}
}
/**
* Removes all the groups with no entries.
*
* This is used by {@link RosterPushListener} and {@link RosterResultListener} to
* cleanup groups after removing contacts.
*/
private void removeEmptyGroups() {
// 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());
}
}
}
/**
* Ignore ItemTypes as of RFC 6121, 2.1.2.5.
*
* This is used by {@link RosterPushListener} and {@link RosterResultListener}.
* */
private static boolean hasValidSubscriptionType(RosterPacket.Item item) {
switch (item.getItemType()) {
case none:
case from:
case to:
case both:
return true;
default:
return false;
}
}
/**
* Check if the server supports roster versioning.
*
* @return true if the server supports roster versioning, false otherwise.
*/
public boolean isRosterVersioningSupported() {
return connection().hasFeature(RosterVer.ELEMENT, RosterVer.NAMESPACE);
}
/**
* 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 {
/**
* Retrieve the user presences (a map from resource to {@link Presence}) for a given key (usually a JID without
* a resource). If the {@link #presenceMap} does not contain already a user presence map, then it will be
* created.
*
* @param key the presence map key
* @return the user presences
*/
private Map<String, Presence> getUserPresences(String key) {
Map<String, Presence> userPresences = presenceMap.get(key);
if (userPresences == null) {
userPresences = new ConcurrentHashMap<>();
presenceMap.put(key, userPresences);
}
return userPresences;
}
@Override
public void processPacket(Stanza packet) throws NotConnectedException {
final XMPPConnection connection = connection();
Presence presence = (Presence) packet;
String from = presence.getFrom();
String key = getMapKey(from);
Map<String, Presence> userPresences;
Presence response = null;
// 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.
switch (presence.getType()) {
case available:
// Get the user presence map
userPresences = getUserPresences(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(XmppStringUtils.parseResource(from), presence);
// If the user is in the roster, fire an event.
if (entries.containsKey(key)) {
fireRosterPresenceEvent(presence);
}
break;
// If an "unavailable" packet.
case 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(XmppStringUtils.parseResource(from))) {
// Get the user presence map
userPresences = getUserPresences(key);
userPresences.put("", presence);
}
// Otherwise, this is a normal offline presence.
else if (presenceMap.get(key) != null) {
userPresences = presenceMap.get(key);
// Store the offline presence, as it may include extra information
// such as the user being on vacation.
userPresences.put(XmppStringUtils.parseResource(from), presence);
}
// If the user is in the roster, fire an event.
if (entries.containsKey(key)) {
fireRosterPresenceEvent(presence);
}
break;
case subscribe:
switch (subscriptionMode) {
case accept_all:
// Accept all subscription requests.
response = new Presence(Presence.Type.subscribed);
break;
case reject_all:
// Reject all subscription requests.
response = new Presence(Presence.Type.unsubscribed);
break;
case manual:
default:
// Otherwise, in manual mode so ignore.
break;
}
if (response != null) {
response.setTo(presence.getFrom());
connection.sendPacket(response);
}
break;
case 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.
response = new Presence(Presence.Type.unsubscribed);
response.setTo(presence.getFrom());
connection.sendPacket(response);
}
// Otherwise, in manual mode so ignore.
break;
// Error presence packets from a bare JID mean we invalidate all existing
// presence info for the user.
case error:
if (!"".equals(XmppStringUtils.parseResource(from))) {
break;
}
userPresences = getUserPresences(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.
if (entries.containsKey(key)) {
fireRosterPresenceEvent(presence);
}
break;
default:
break;
}
}
}
/**
* Handles roster reults as described in RFC 6121 2.1.4
*/
private class RosterResultListener implements PacketListener {
@Override
public void processPacket(Stanza packet) {
final XMPPConnection connection = connection();
LOGGER.fine("RosterResultListener received stanza");
Collection<String> addedEntries = new ArrayList<String>();
Collection<String> updatedEntries = new ArrayList<String>();
Collection<String> deletedEntries = new ArrayList<String>();
Collection<String> unchangedEntries = new ArrayList<String>();
if (packet instanceof RosterPacket) {
// Non-empty roster result. This stanza contains all the roster elements.
RosterPacket rosterPacket = (RosterPacket) packet;
// Ignore items without valid subscription type
ArrayList<Item> validItems = new ArrayList<RosterPacket.Item>();
for (RosterPacket.Item item : rosterPacket.getRosterItems()) {
if (hasValidSubscriptionType(item)) {
validItems.add(item);
}
}
for (RosterPacket.Item item : validItems) {
RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
item.getItemType(), item.getItemStatus(), Roster.this, connection);
addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
}
// Delete all entries which where not added or updated
Set<String> toDelete = new HashSet<String>();
for (RosterEntry entry : entries.values()) {
toDelete.add(entry.getUser());
}
toDelete.removeAll(addedEntries);
toDelete.removeAll(updatedEntries);
toDelete.removeAll(unchangedEntries);
for (String user : toDelete) {
deleteEntry(deletedEntries, entries.get(user));
}
if (rosterStore != null) {
String version = rosterPacket.getVersion();
rosterStore.resetEntries(validItems, version);
}
removeEmptyGroups();
}
else {
// Empty roster result as defined in RFC6121 2.6.3. An empty roster result basically
// means that rosterver was used and the roster hasn't changed (much) since the
// version we presented the server. So we simply load the roster from the store and
// await possible further roster pushes.
for (RosterPacket.Item item : rosterStore.getEntries()) {
RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
item.getItemType(), item.getItemStatus(), Roster.this, connection);
addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
}
}
loaded = true;
synchronized (Roster.this) {
Roster.this.notifyAll();
}
// Fire event for roster listeners.
fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
}
}
/**
* Listens for all roster pushes and processes them.
*/
private class RosterPushListener extends AbstractIqRequestHandler {
private RosterPushListener() {
super(RosterPacket.ELEMENT, RosterPacket.NAMESPACE, Type.set, Mode.sync);
}
@Override
public IQ handleIQRequest(IQ iqRequest) {
final XMPPConnection connection = connection();
RosterPacket rosterPacket = (RosterPacket) iqRequest;
// Roster push (RFC 6121, 2.1.6)
// A roster push with a non-empty from not matching our address MUST be ignored
String jid = XmppStringUtils.parseBareJid(connection.getUser());
String from = rosterPacket.getFrom();
if (from != null && !from.equals(jid)) {
LOGGER.warning("Ignoring roster push with a non matching 'from' ourJid='" + jid + "' from='" + from
+ "'");
return IQ.createErrorResponse(iqRequest, new XMPPError(Condition.service_unavailable));
}
// A roster push must contain exactly one entry
Collection<Item> items = rosterPacket.getRosterItems();
if (items.size() != 1) {
LOGGER.warning("Ignoring roster push with not exaclty one entry. size=" + items.size());
return IQ.createErrorResponse(iqRequest, new XMPPError(Condition.bad_request));
}
Collection<String> addedEntries = new ArrayList<String>();
Collection<String> updatedEntries = new ArrayList<String>();
Collection<String> deletedEntries = new ArrayList<String>();
Collection<String> unchangedEntries = new ArrayList<String>();
// We assured above that the size of items is exaclty 1, therefore we are able to
// safely retrieve this single item here.
Item item = items.iterator().next();
RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
item.getItemType(), item.getItemStatus(), Roster.this, connection);
String version = rosterPacket.getVersion();
if (item.getItemType().equals(RosterPacket.ItemType.remove)) {
deleteEntry(deletedEntries, entry);
if (rosterStore != null) {
rosterStore.removeEntry(entry.getUser(), version);
}
}
else if (hasValidSubscriptionType(item)) {
addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
if (rosterStore != null) {
rosterStore.addEntry(item, version);
}
}
removeEmptyGroups();
// Fire event for roster listeners.
fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
return IQ.createResultIQ(rosterPacket);
}
}
}