/** * * Copyright 2003-2007 Jive Software, 2016-2019 Florian Schmaus. * * 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.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.AbstractConnectionListener; import org.jivesoftware.smack.AsyncButOrdered; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.FeatureNotSupportedException; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.SmackException.NotLoggedInException; import org.jivesoftware.smack.SmackFuture; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.filter.AndFilter; import org.jivesoftware.smack.filter.PresenceTypeFilter; import org.jivesoftware.smack.filter.StanzaFilter; import org.jivesoftware.smack.filter.StanzaTypeFilter; import org.jivesoftware.smack.filter.ToMatchesFilter; import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ.Type; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StanzaError.Condition; import org.jivesoftware.smack.roster.SubscribeListener.SubscribeAnswer; import org.jivesoftware.smack.roster.packet.RosterPacket; import org.jivesoftware.smack.roster.packet.RosterPacket.Item; import org.jivesoftware.smack.roster.packet.RosterVer; import org.jivesoftware.smack.roster.packet.SubscriptionPreApproval; import org.jivesoftware.smack.roster.rosterstore.RosterStore; import org.jivesoftware.smack.util.ExceptionCallback; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.SuccessCallback; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.EntityFullJid; import org.jxmpp.jid.FullJid; import org.jxmpp.jid.Jid; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.util.cache.LruCache; /** * 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. * * Other users may attempt to subscribe to this user using a subscription request. Three * modes are supported for handling these requests:
* This method will never return null
, instead if the user has not yet logged into
* the server all modifying methods of the returned roster object
* like {@link Roster#createEntry(BareJid, String, String[])},
* {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing
* {@link RosterListener}s will throw an IllegalStateException.
*
* 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#reject_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(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 * @throws InterruptedException */ public void reload() throws NotLoggedInException, NotConnectedException, InterruptedException { final XMPPConnection connection = getAuthenticatedConnectionOrThrow(); RosterPacket packet = new RosterPacket(); if (rosterStore != null && isRosterVersioningSupported()) { packet.setVersion(rosterStore.getRosterVersion()); } rosterState = RosterState.loading; SmackFuture* Among those events are: *
* 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 */ public RosterGroup createGroup(String name) { final XMPPConnection connection = connection(); 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, ornull
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
* @throws InterruptedException
* @deprecated use {@link #createItemAndRequestSubscription(BareJid, String, String[])} instead.
*/
// TODO: Remove in Smack 4.5.
@Deprecated
public void createEntry(BareJid user, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
createItemAndRequestSubscription(user, name, groups);
}
/**
* Creates a new roster item. The server will asynchronously update the roster with the subscription status.
* * There will be no presence subscription request. Consider using * {@link #createItemAndRequestSubscription(BareJid, String, String[])} if you also want to request a presence * subscription from the contact. *
* * @param jid the XMPP address of the contact (e.g. johndoe@jabber.org) * @param name the nickname of the user. * @param groups the list of group names the entry will belong to, ornull
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
* @throws InterruptedException
* @since 4.4.0
*/
public void createItem(BareJid jid, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
final XMPPConnection connection = getAuthenticatedConnectionOrThrow();
// Create and send roster entry creation packet.
RosterPacket rosterPacket = new RosterPacket();
rosterPacket.setType(IQ.Type.set);
RosterPacket.Item item = new RosterPacket.Item(jid, name);
if (groups != null) {
for (String group : groups) {
if (group != null && group.trim().length() > 0) {
item.addGroupName(group);
}
}
}
rosterPacket.addRosterItem(item);
connection.createStanzaCollectorAndSend(rosterPacket).nextResultOrThrow();
}
/**
* Creates a new roster entry and presence subscription. The server will asynchronously
* update the roster with the subscription status.
*
* @param jid the XMPP address of the contact (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.
* @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
* @throws InterruptedException
* @since 4.4.0
*/
public void createItemAndRequestSubscription(BareJid jid, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
createItem(jid, name, groups);
sendSubscriptionRequest(jid);
}
/**
* Creates a new pre-approved 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.
* @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
* @throws InterruptedException
* @throws FeatureNotSupportedException if pre-approving is not supported.
* @since 4.2
*/
public void preApproveAndCreateEntry(BareJid user, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, FeatureNotSupportedException {
preApprove(user);
createItemAndRequestSubscription(user, name, groups);
}
/**
* Pre-approve user presence subscription.
*
* @param user the user. (e.g. johndoe@jabber.org)
* @throws NotLoggedInException if not logged in.
* @throws NotConnectedException
* @throws InterruptedException
* @throws FeatureNotSupportedException if pre-approving is not supported.
* @since 4.2
*/
public void preApprove(BareJid user) throws NotLoggedInException, NotConnectedException, InterruptedException, FeatureNotSupportedException {
final XMPPConnection connection = connection();
if (!isSubscriptionPreApprovalSupported()) {
throw new FeatureNotSupportedException("Pre-approving");
}
Presence presencePacket = new Presence(Presence.Type.subscribed);
presencePacket.setTo(user);
connection.sendStanza(presencePacket);
}
/**
* Check for subscription pre-approval support.
*
* @return true if subscription pre-approval is supported by the server.
* @throws NotLoggedInException if not logged in.
* @since 4.2
*/
public boolean isSubscriptionPreApprovalSupported() throws NotLoggedInException {
final XMPPConnection connection = getAuthenticatedConnectionOrThrow();
return connection.hasFeature(SubscriptionPreApproval.ELEMENT, SubscriptionPreApproval.NAMESPACE);
}
public void sendSubscriptionRequest(BareJid jid) throws NotLoggedInException, NotConnectedException, InterruptedException {
final XMPPConnection connection = getAuthenticatedConnectionOrThrow();
// Create a presence subscription packet and send.
Presence presencePacket = new Presence(Presence.Type.subscribe);
presencePacket.setTo(jid);
connection.sendStanza(presencePacket);
}
/**
* Add a subscribe listener, which is invoked on incoming subscription requests and if
* {@link SubscriptionMode} is set to {@link SubscriptionMode#manual}. This also sets subscription
* mode to {@link SubscriptionMode#manual}.
*
* @param subscribeListener the subscribe listener to add.
* @return true
if the listener was not already added.
* @since 4.2
*/
public boolean addSubscribeListener(SubscribeListener subscribeListener) {
Objects.requireNonNull(subscribeListener, "SubscribeListener argument must not be null");
if (subscriptionMode != SubscriptionMode.manual) {
previousSubscriptionMode = subscriptionMode;
subscriptionMode = SubscriptionMode.manual;
}
return subscribeListeners.add(subscribeListener);
}
/**
* Remove a subscribe listener. Also restores the previous subscription mode
* state, if the last listener got removed.
*
* @param subscribeListener
* the subscribe listener to remove.
* @return true
if the listener registered and got removed.
* @since 4.2
*/
public boolean removeSubscribeListener(SubscribeListener subscribeListener) {
boolean removed = subscribeListeners.remove(subscribeListener);
if (removed && subscribeListeners.isEmpty()) {
setSubscriptionMode(previousSubscriptionMode);
}
return removed;
}
/**
* 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 InterruptedException
*/
public void removeEntry(RosterEntry entry) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
final XMPPConnection connection = getAuthenticatedConnectionOrThrow();
// 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.getJid())) {
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.createStanzaCollectorAndSend(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.
*
* The method guarantees that the listener is only invoked after
* {@link RosterEntries#rosterEntries(Collection)} has been invoked, and that all roster events
* that happen while rosterEntries(Collection)
is called are queued until the
* method returns.
*
* 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. *
* * @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.rosterEntries(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 Setnull
if the user is not an entry in the roster.
*
* @param jid 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(BareJid jid) {
if (jid == null) {
return null;
}
return entries.get(jid);
}
/**
* Returns true if the specified XMPP address is an entry in the roster.
*
* @param jid the XMPP address of the user (eg "jsmith@example.com"). The
* address must be a bare JID e.g. "domain/resource" or
* "user@domain".
* @return true if the XMPP address is an entry in the roster.
*/
public boolean contains(BareJid jid) {
return getEntry(jid) != null;
}
/**
* 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) {
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* 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}.
*
** 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 jid the XMPP address of the user (eg "jsmith@example.com"). The * address must be a bare JID e.g. "domain/resource" or * "user@domain". * @return the user's current presence, or unavailable presence if the user is offline * or if no presence information is available.. */ public Presence getPresence(BareJid jid) { Map* 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. *
* 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(Jid jid) { if (jid == null) { return false; } BareJid bareJid = jid.asBareJid(); if (connection().getXMPPServiceDomain().equals(bareJid)) { return true; } RosterEntry entry = getEntry(bareJid); if (entry == null) { return false; } return entry.canSeeMyPresence(); } /** * Check if the XMPP entity this roster belongs to is subscribed to the presence of the given JID. * * @param jid the jid to check. * @returntrue
if we are subscribed to the presence of the given jid.
* @since 4.2
*/
public boolean iAmSubscribedTo(Jid jid) {
if (jid == null) {
return false;
}
BareJid bareJid = jid.asBareJid();
RosterEntry entry = getEntry(bareJid);
if (entry == null) {
return false;
}
return entry.canSeeHisPresence();
}
/**
* Sets if the roster will be loaded from the server when logging in for newly created instances
* of {@link Roster}.
*
* @param rosterLoadedAtLoginDefault if the roster will be loaded from the server when logging in.
* @see #setRosterLoadedAtLogin(boolean)
* @since 4.1.7
*/
public static void setRosterLoadedAtLoginDefault(boolean rosterLoadedAtLoginDefault) {
Roster.rosterLoadedAtLoginDefault = rosterLoadedAtLoginDefault;
}
/**
* 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 RFC 6121 2.2 - Retrieving the Roster on Login
*/
public boolean isRosterLoadedAtLogin() {
return rosterLoadedAtLogin;
}
RosterStore getRosterStore() {
return rosterStore;
}
/**
* Changes the presence of available contacts offline by simulating an unavailable
* presence sent from the server.
*/
private void setOfflinePresences() {
Presence packetUnavailable;
outerloop: for (Jid user : presenceMap.keySet()) {
Map* The roster will only store this many presence entries for entities non in the Roster. The * default is {@value #INITIAL_DEFAULT_NON_ROSTER_PRESENCE_MAP_SIZE}. *
* * @param maximumSize the maximum size * @since 4.2 */ public static void setDefaultNonRosterPresenceMapMaxSize(int maximumSize) { defaultNonRosterPresenceMapMaxSize = maximumSize; } /** * Set the maximum size of the non-Roster presence map. * * @param maximumSize * @since 4.2 * @see #setDefaultNonRosterPresenceMapMaxSize(int) */ public void setNonRosterPresenceMapMaxSize(int maximumSize) { nonRosterPresenceMap.setMaxCacheSize(maximumSize); } }