001/** 002 * 003 * Copyright 2003-2007 Jive Software, 2016-2021 Florian Schmaus. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.jivesoftware.smack.roster; 019 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashSet; 025import java.util.LinkedHashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.WeakHashMap; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.CopyOnWriteArraySet; 032import java.util.logging.Level; 033import java.util.logging.Logger; 034 035import org.jivesoftware.smack.AsyncButOrdered; 036import org.jivesoftware.smack.ConnectionCreationListener; 037import org.jivesoftware.smack.ConnectionListener; 038import org.jivesoftware.smack.Manager; 039import org.jivesoftware.smack.SmackException; 040import org.jivesoftware.smack.SmackException.FeatureNotSupportedException; 041import org.jivesoftware.smack.SmackException.NoResponseException; 042import org.jivesoftware.smack.SmackException.NotConnectedException; 043import org.jivesoftware.smack.SmackException.NotLoggedInException; 044import org.jivesoftware.smack.SmackFuture; 045import org.jivesoftware.smack.StanzaListener; 046import org.jivesoftware.smack.XMPPConnection; 047import org.jivesoftware.smack.XMPPConnectionRegistry; 048import org.jivesoftware.smack.XMPPException.XMPPErrorException; 049import org.jivesoftware.smack.filter.AndFilter; 050import org.jivesoftware.smack.filter.PresenceTypeFilter; 051import org.jivesoftware.smack.filter.StanzaFilter; 052import org.jivesoftware.smack.filter.StanzaTypeFilter; 053import org.jivesoftware.smack.filter.ToMatchesFilter; 054import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; 055import org.jivesoftware.smack.packet.IQ; 056import org.jivesoftware.smack.packet.Presence; 057import org.jivesoftware.smack.packet.PresenceBuilder; 058import org.jivesoftware.smack.packet.Stanza; 059import org.jivesoftware.smack.packet.StanzaBuilder; 060import org.jivesoftware.smack.packet.StanzaError.Condition; 061import org.jivesoftware.smack.roster.SubscribeListener.SubscribeAnswer; 062import org.jivesoftware.smack.roster.packet.RosterPacket; 063import org.jivesoftware.smack.roster.packet.RosterPacket.Item; 064import org.jivesoftware.smack.roster.packet.RosterVer; 065import org.jivesoftware.smack.roster.packet.SubscriptionPreApproval; 066import org.jivesoftware.smack.roster.rosterstore.RosterStore; 067import org.jivesoftware.smack.util.ExceptionCallback; 068import org.jivesoftware.smack.util.Objects; 069import org.jivesoftware.smack.util.SuccessCallback; 070 071import org.jxmpp.jid.BareJid; 072import org.jxmpp.jid.EntityBareJid; 073import org.jxmpp.jid.EntityFullJid; 074import org.jxmpp.jid.FullJid; 075import org.jxmpp.jid.Jid; 076import org.jxmpp.jid.impl.JidCreate; 077import org.jxmpp.jid.parts.Resourcepart; 078import org.jxmpp.util.cache.LruCache; 079 080/** 081 * Represents a user's roster, which is the collection of users a person receives 082 * presence updates for. Roster items are categorized into groups for easier management. 083 * 084 * Other users may attempt to subscribe to this user using a subscription request. Three 085 * modes are supported for handling these requests: <ul> 086 * <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li> 087 * <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li> 088 * <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li> 089 * </ul> 090 * 091 * @author Matt Tucker 092 * @see #getInstanceFor(XMPPConnection) 093 */ 094public final class Roster extends Manager { 095 096 private static final Logger LOGGER = Logger.getLogger(Roster.class.getName()); 097 098 static { 099 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 100 @Override 101 public void connectionCreated(XMPPConnection connection) { 102 getInstanceFor(connection); 103 } 104 }); 105 } 106 107 private static final Map<XMPPConnection, Roster> INSTANCES = new WeakHashMap<>(); 108 109 /** 110 * Returns the roster for the user. 111 * <p> 112 * This method will never return <code>null</code>, instead if the user has not yet logged into 113 * the server all modifying methods of the returned roster object 114 * like {@link Roster#createEntry(BareJid, String, String[])}, 115 * {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing 116 * {@link RosterListener}s will throw an IllegalStateException. 117 * </p> 118 * 119 * @param connection the connection the roster should be retrieved for. 120 * @return the user's roster. 121 */ 122 public static synchronized Roster getInstanceFor(XMPPConnection connection) { 123 Roster roster = INSTANCES.get(connection); 124 if (roster == null) { 125 roster = new Roster(connection); 126 INSTANCES.put(connection, roster); 127 } 128 return roster; 129 } 130 131 private static final StanzaFilter PRESENCE_PACKET_FILTER = StanzaTypeFilter.PRESENCE; 132 133 private static final StanzaFilter OUTGOING_USER_UNAVAILABLE_PRESENCE = new AndFilter(PresenceTypeFilter.UNAVAILABLE, ToMatchesFilter.MATCH_NO_TO_SET); 134 135 private static boolean rosterLoadedAtLoginDefault = true; 136 137 /** 138 * The default subscription processing mode to use when a Roster is created. By default 139 * all subscription requests are automatically rejected. 140 */ 141 private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.reject_all; 142 143 /** 144 * The initial maximum size of the map holding presence information of entities without an Roster entry. Currently 145 * {@value #INITIAL_DEFAULT_NON_ROSTER_PRESENCE_MAP_SIZE}. 146 */ 147 public static final int INITIAL_DEFAULT_NON_ROSTER_PRESENCE_MAP_SIZE = 1024; 148 149 private static int defaultNonRosterPresenceMapMaxSize = INITIAL_DEFAULT_NON_ROSTER_PRESENCE_MAP_SIZE; 150 151 private RosterStore rosterStore; 152 153 /** 154 * The groups of this roster. 155 * <p> 156 * Note that we use {@link ConcurrentHashMap} also as static type of this field, since we use the fact that the same 157 * thread can modify this collection, e.g. remove items, while iterating over it. This is done, for example in 158 * {@link #deleteEntry(Collection, RosterEntry)}. If we do not denote the static type to ConcurrentHashMap, but 159 * {@link Map} instead, then error prone would report a ModifyCollectionInEnhancedForLoop but. 160 * </p> 161 */ 162 private final ConcurrentHashMap<String, RosterGroup> groups = new ConcurrentHashMap<>(); 163 164 /** 165 * Concurrent hash map from JID to its roster entry. 166 */ 167 private final Map<BareJid, RosterEntry> entries = new ConcurrentHashMap<>(); 168 169 private final Set<RosterEntry> unfiledEntries = new CopyOnWriteArraySet<>(); 170 private final Set<RosterListener> rosterListeners = new LinkedHashSet<>(); 171 172 private final Set<PresenceEventListener> presenceEventListeners = new CopyOnWriteArraySet<>(); 173 174 /** 175 * A map of JIDs to another Map of Resourceparts to Presences. The 'inner' map may contain 176 * {@link Resourcepart#EMPTY} if there are no other Presences available. 177 */ 178 private final Map<BareJid, Map<Resourcepart, Presence>> presenceMap = new ConcurrentHashMap<>(); 179 180 /** 181 * Like {@link presenceMap} but for presences of entities not in our Roster. 182 */ 183 // TODO Ideally we want here to use a LRU cache like Map which will evict all superfluous items 184 // if their maximum size is lowered below the current item count. LruCache does not provide 185 // this. 186 private final LruCache<BareJid, Map<Resourcepart, Presence>> nonRosterPresenceMap = new LruCache<>( 187 defaultNonRosterPresenceMapMaxSize); 188 189 /** 190 * Listeners called when the Roster was loaded. 191 */ 192 private final Set<RosterLoadedListener> rosterLoadedListeners = new LinkedHashSet<>(); 193 194 /** 195 * Mutually exclude roster listener invocation and changing the {@link entries} map. Also used 196 * to synchronize access to either the roster listeners or the entries map. 197 */ 198 private final Object rosterListenersAndEntriesLock = new Object(); 199 200 private enum RosterState { 201 uninitialized, 202 loading, 203 loaded, 204 } 205 206 /** 207 * The current state of the roster. 208 */ 209 private RosterState rosterState = RosterState.uninitialized; 210 211 private final PresencePacketListener presencePacketListener = new PresencePacketListener(); 212 213 /** 214 * 215 */ 216 private boolean rosterLoadedAtLogin = rosterLoadedAtLoginDefault; 217 218 private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode(); 219 220 private final Set<SubscribeListener> subscribeListeners = new CopyOnWriteArraySet<>(); 221 222 private SubscriptionMode previousSubscriptionMode; 223 224 /** 225 * Returns the default subscription processing mode to use when a new Roster is created. The 226 * subscription processing mode dictates what action Smack will take when subscription 227 * requests from other users are made. The default subscription mode 228 * is {@link SubscriptionMode#reject_all}. 229 * 230 * @return the default subscription mode to use for new Rosters 231 */ 232 public static SubscriptionMode getDefaultSubscriptionMode() { 233 return defaultSubscriptionMode; 234 } 235 236 /** 237 * Sets the default subscription processing mode to use when a new Roster is created. The 238 * subscription processing mode dictates what action Smack will take when subscription 239 * requests from other users are made. The default subscription mode 240 * is {@link SubscriptionMode#reject_all}. 241 * 242 * @param subscriptionMode the default subscription mode to use for new Rosters. 243 */ 244 public static void setDefaultSubscriptionMode(SubscriptionMode subscriptionMode) { 245 defaultSubscriptionMode = subscriptionMode; 246 } 247 248 private final AsyncButOrdered<BareJid> asyncButOrdered = new AsyncButOrdered<>(); 249 250 /** 251 * Creates a new roster. 252 * 253 * @param connection an XMPP connection. 254 */ 255 private Roster(final XMPPConnection connection) { 256 super(connection); 257 258 // Note that we use sync packet listeners because RosterListeners should be invoked in the same order as the 259 // roster stanzas arrive. 260 // Listen for any roster packets. 261 connection.registerIQRequestHandler(new RosterPushListener()); 262 // Listen for any presence packets. 263 connection.addSyncStanzaListener(presencePacketListener, PRESENCE_PACKET_FILTER); 264 265 connection.addAsyncStanzaListener(new StanzaListener() { 266 @SuppressWarnings("fallthrough") 267 @Override 268 public void processStanza(Stanza stanza) throws NotConnectedException, 269 InterruptedException, NotLoggedInException { 270 Presence presence = (Presence) stanza; 271 Jid from = presence.getFrom(); 272 SubscribeAnswer subscribeAnswer = null; 273 switch (subscriptionMode) { 274 case manual: 275 for (SubscribeListener subscribeListener : subscribeListeners) { 276 subscribeAnswer = subscribeListener.processSubscribe(from, presence); 277 if (subscribeAnswer != null) { 278 break; 279 } 280 } 281 if (subscribeAnswer == null) { 282 return; 283 } 284 break; 285 case accept_all: 286 // Accept all subscription requests. 287 subscribeAnswer = SubscribeAnswer.Approve; 288 break; 289 case reject_all: 290 // Reject all subscription requests. 291 subscribeAnswer = SubscribeAnswer.Deny; 292 break; 293 } 294 295 if (subscribeAnswer == null) { 296 return; 297 } 298 299 Presence.Type type; 300 switch (subscribeAnswer) { 301 case ApproveAndAlsoRequestIfRequired: 302 BareJid bareFrom = from.asBareJid(); 303 RosterUtil.askForSubscriptionIfRequired(Roster.this, bareFrom); 304 // The fall through is intended. 305 case Approve: 306 type = Presence.Type.subscribed; 307 break; 308 case Deny: 309 type = Presence.Type.unsubscribed; 310 break; 311 default: 312 throw new AssertionError(); 313 } 314 315 Presence response = connection.getStanzaFactory().buildPresenceStanza() 316 .ofType(type) 317 .to(presence.getFrom()) 318 .build(); 319 connection.sendStanza(response); 320 } 321 }, PresenceTypeFilter.SUBSCRIBE); 322 323 // Listen for connection events 324 connection.addConnectionListener(new ConnectionListener() { 325 326 @Override 327 public void authenticated(XMPPConnection connection, boolean resumed) { 328 if (!isRosterLoadedAtLogin()) 329 return; 330 // We are done here if the connection was resumed 331 if (resumed) { 332 return; 333 } 334 335 // Ensure that all available presences received so far in a eventually existing previous session are 336 // marked 'offline'. 337 setOfflinePresencesAndResetLoaded(); 338 339 try { 340 Roster.this.reload(); 341 } 342 catch (InterruptedException | SmackException e) { 343 LOGGER.log(Level.SEVERE, "Could not reload Roster", e); 344 return; 345 } 346 } 347 348 @Override 349 public void connectionClosed() { 350 // Changes the presence available contacts to unavailable 351 setOfflinePresencesAndResetLoaded(); 352 } 353 354 }); 355 356 connection.addStanzaSendingListener(new StanzaListener() { 357 @Override 358 public void processStanza(Stanza stanzav) throws NotConnectedException, InterruptedException { 359 // Once we send an unavailable presence, the server is allowed to suppress sending presence status 360 // information to us as optimization (RFC 6121 § 4.4.2). Thus XMPP clients which are unavailable, should 361 // consider the presence information of their contacts as not up-to-date. We make the user obvious of 362 // this situation by setting the presences of all contacts to unavailable (while keeping the roster 363 // state). 364 setOfflinePresences(); 365 } 366 }, OUTGOING_USER_UNAVAILABLE_PRESENCE); 367 368 // If the connection is already established, call reload 369 if (connection.isAuthenticated()) { 370 try { 371 reloadAndWait(); 372 } 373 catch (InterruptedException | SmackException e) { 374 LOGGER.log(Level.SEVERE, "Could not reload Roster", e); 375 } 376 } 377 378 } 379 380 /** 381 * Retrieve the user presences (a map from resource to {@link Presence}) for a given XMPP entity represented by their bare JID. 382 * 383 * @param entity the entity 384 * @return the user presences 385 */ 386 private Map<Resourcepart, Presence> getPresencesInternal(BareJid entity) { 387 Map<Resourcepart, Presence> entityPresences = presenceMap.get(entity); 388 if (entityPresences == null) { 389 entityPresences = nonRosterPresenceMap.lookup(entity); 390 } 391 return entityPresences; 392 } 393 394 /** 395 * Retrieve the user presences (a map from resource to {@link Presence}) for a given XMPP entity represented by their bare JID. 396 * 397 * @param entity the entity 398 * @return the user presences 399 */ 400 private synchronized Map<Resourcepart, Presence> getOrCreatePresencesInternal(BareJid entity) { 401 Map<Resourcepart, Presence> entityPresences = getPresencesInternal(entity); 402 if (entityPresences == null) { 403 if (contains(entity)) { 404 entityPresences = new ConcurrentHashMap<>(); 405 presenceMap.put(entity, entityPresences); 406 } 407 else { 408 LruCache<Resourcepart, Presence> nonRosterEntityPresences = new LruCache<>(32); 409 nonRosterPresenceMap.put(entity, nonRosterEntityPresences); 410 entityPresences = nonRosterEntityPresences; 411 } 412 } 413 return entityPresences; 414 } 415 416 /** 417 * Returns the subscription processing mode, which dictates what action 418 * Smack will take when subscription requests from other users are made. 419 * The default subscription mode is {@link SubscriptionMode#reject_all}. 420 * <p> 421 * If using the manual mode, a PacketListener should be registered that 422 * listens for Presence packets that have a type of 423 * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. 424 * </p> 425 * 426 * @return the subscription mode. 427 */ 428 public SubscriptionMode getSubscriptionMode() { 429 return subscriptionMode; 430 } 431 432 /** 433 * Sets the subscription processing mode, which dictates what action 434 * Smack will take when subscription requests from other users are made. 435 * The default subscription mode is {@link SubscriptionMode#reject_all}. 436 * <p> 437 * If using the manual mode, a PacketListener should be registered that 438 * listens for Presence packets that have a type of 439 * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. 440 * </p> 441 * 442 * @param subscriptionMode the subscription mode. 443 */ 444 public void setSubscriptionMode(SubscriptionMode subscriptionMode) { 445 this.subscriptionMode = subscriptionMode; 446 } 447 448 /** 449 * Reloads the entire roster from the server. This is an asynchronous operation, 450 * which means the method will return immediately, and the roster will be 451 * reloaded at a later point when the server responds to the reload request. 452 * @throws NotLoggedInException If not logged in. 453 * @throws NotConnectedException if the XMPP connection is not connected. 454 * @throws InterruptedException if the calling thread was interrupted. 455 */ 456 public void reload() throws NotLoggedInException, NotConnectedException, InterruptedException { 457 final XMPPConnection connection = getAuthenticatedConnectionOrThrow(); 458 459 RosterPacket packet = new RosterPacket(); 460 if (rosterStore != null && isRosterVersioningSupported()) { 461 packet.setVersion(rosterStore.getRosterVersion()); 462 } 463 rosterState = RosterState.loading; 464 465 SmackFuture<IQ, Exception> future = connection.sendIqRequestAsync(packet); 466 467 future.onSuccess(new RosterResultListener()).onError(new ExceptionCallback<Exception>() { 468 469 @Override 470 public void processException(Exception exception) { 471 rosterState = RosterState.uninitialized; 472 Level logLevel = Level.SEVERE; 473 if (exception instanceof NotConnectedException) { 474 logLevel = Level.FINE; 475 } else if (exception instanceof XMPPErrorException) { 476 Condition condition = ((XMPPErrorException) exception).getStanzaError().getCondition(); 477 if (condition == Condition.feature_not_implemented || condition == Condition.service_unavailable) { 478 logLevel = Level.FINE; 479 } 480 } 481 LOGGER.log(logLevel, "Exception reloading roster", exception); 482 for (RosterLoadedListener listener : rosterLoadedListeners) { 483 listener.onRosterLoadingFailed(exception); 484 } 485 } 486 487 }); 488 } 489 490 /** 491 * Reload the roster and block until it is reloaded. 492 * 493 * @throws NotLoggedInException if the XMPP connection is not authenticated. 494 * @throws NotConnectedException if the XMPP connection is not connected. 495 * @throws InterruptedException if the calling thread was interrupted. 496 * @since 4.1 497 */ 498 public void reloadAndWait() throws NotLoggedInException, NotConnectedException, InterruptedException { 499 reload(); 500 waitUntilLoaded(); 501 } 502 503 /** 504 * Set the roster store, may cause a roster reload. 505 * 506 * @param rosterStore TODO javadoc me please 507 * @return true if the roster reload was initiated, false otherwise. 508 * @since 4.1 509 */ 510 public boolean setRosterStore(RosterStore rosterStore) { 511 this.rosterStore = rosterStore; 512 try { 513 reload(); 514 } 515 catch (InterruptedException | NotLoggedInException | NotConnectedException e) { 516 LOGGER.log(Level.FINER, "Could not reload roster", e); 517 return false; 518 } 519 return true; 520 } 521 522 boolean waitUntilLoaded() throws InterruptedException { 523 long waitTime = connection().getReplyTimeout(); 524 long start = System.currentTimeMillis(); 525 while (!isLoaded()) { 526 if (waitTime <= 0) { 527 break; 528 } 529 synchronized (this) { 530 if (!isLoaded()) { 531 wait(waitTime); 532 } 533 } 534 long now = System.currentTimeMillis(); 535 waitTime -= now - start; 536 start = now; 537 } 538 return isLoaded(); 539 } 540 541 /** 542 * Check if the roster is loaded. 543 * 544 * @return true if the roster is loaded. 545 * @since 4.1 546 */ 547 public boolean isLoaded() { 548 return rosterState == RosterState.loaded; 549 } 550 551 /** 552 * Adds a listener to this roster. The listener will be fired anytime one or more 553 * changes to the roster are pushed from the server. 554 * 555 * @param rosterListener a roster listener. 556 * @return true if the listener was not already added. 557 * @see #getEntriesAndAddListener(RosterListener, RosterEntries) 558 */ 559 public boolean addRosterListener(RosterListener rosterListener) { 560 synchronized (rosterListenersAndEntriesLock) { 561 return rosterListeners.add(rosterListener); 562 } 563 } 564 565 /** 566 * Removes a listener from this roster. The listener will be fired anytime one or more 567 * changes to the roster are pushed from the server. 568 * 569 * @param rosterListener a roster listener. 570 * @return true if the listener was active and got removed. 571 */ 572 public boolean removeRosterListener(RosterListener rosterListener) { 573 synchronized (rosterListenersAndEntriesLock) { 574 return rosterListeners.remove(rosterListener); 575 } 576 } 577 578 /** 579 * Add a roster loaded listener. Roster loaded listeners are invoked once the {@link Roster} 580 * was successfully loaded. 581 * 582 * @param rosterLoadedListener the listener to add. 583 * @return true if the listener was not already added. 584 * @see RosterLoadedListener 585 * @since 4.1 586 */ 587 public boolean addRosterLoadedListener(RosterLoadedListener rosterLoadedListener) { 588 synchronized (rosterLoadedListener) { 589 return rosterLoadedListeners.add(rosterLoadedListener); 590 } 591 } 592 593 /** 594 * Remove a roster loaded listener. 595 * 596 * @param rosterLoadedListener the listener to remove. 597 * @return true if the listener was active and got removed. 598 * @see RosterLoadedListener 599 * @since 4.1 600 */ 601 public boolean removeRosterLoadedListener(RosterLoadedListener rosterLoadedListener) { 602 synchronized (rosterLoadedListener) { 603 return rosterLoadedListeners.remove(rosterLoadedListener); 604 } 605 } 606 607 /** 608 * Add a {@link PresenceEventListener}. Such a listener will be fired whenever certain 609 * presence events happen.<p> 610 * Among those events are: 611 * <ul> 612 * <li> 'available' presence received 613 * <li> 'unavailable' presence received 614 * <li> 'error' presence received 615 * <li> 'subscribed' presence received 616 * <li> 'unsubscribed' presence received 617 * </ul> 618 * @param presenceEventListener listener to add. 619 * @return true if the listener was not already added. 620 */ 621 public boolean addPresenceEventListener(PresenceEventListener presenceEventListener) { 622 return presenceEventListeners.add(presenceEventListener); 623 } 624 625 public boolean removePresenceEventListener(PresenceEventListener presenceEventListener) { 626 return presenceEventListeners.remove(presenceEventListener); 627 } 628 629 /** 630 * Creates a new group. 631 * <p> 632 * Note: you must add at least one entry to the group for the group to be kept 633 * after a logout/login. This is due to the way that XMPP stores group information. 634 * </p> 635 * 636 * @param name the name of the group. 637 * @return a new group, or null if the group already exists 638 */ 639 public RosterGroup createGroup(String name) { 640 final XMPPConnection connection = connection(); 641 if (groups.containsKey(name)) { 642 return groups.get(name); 643 } 644 645 RosterGroup group = new RosterGroup(name, connection); 646 groups.put(name, group); 647 return group; 648 } 649 650 /** 651 * Creates a new roster entry and presence subscription. The server will asynchronously 652 * update the roster with the subscription status. 653 * 654 * @param user the user. (e.g. johndoe@jabber.org) 655 * @param name the nickname of the user. 656 * @param groups the list of group names the entry will belong to, or <code>null</code> if the 657 * the roster entry won't belong to a group. 658 * @throws NoResponseException if there was no response from the server. 659 * @throws XMPPErrorException if an XMPP exception occurs. 660 * @throws NotLoggedInException If not logged in. 661 * @throws NotConnectedException if the XMPP connection is not connected. 662 * @throws InterruptedException if the calling thread was interrupted. 663 * @deprecated use {@link #createItemAndRequestSubscription(BareJid, String, String[])} instead. 664 */ 665 // TODO: Remove in Smack 4.5. 666 @Deprecated 667 public void createEntry(BareJid user, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 668 createItemAndRequestSubscription(user, name, groups); 669 } 670 671 /** 672 * Creates a new roster item. The server will asynchronously update the roster with the subscription status. 673 * <p> 674 * There will be no presence subscription request. Consider using 675 * {@link #createItemAndRequestSubscription(BareJid, String, String[])} if you also want to request a presence 676 * subscription from the contact. 677 * </p> 678 * 679 * @param jid the XMPP address of the contact (e.g. johndoe@jabber.org) 680 * @param name the nickname of the user. 681 * @param groups the list of group names the entry will belong to, or <code>null</code> if the the roster entry won't 682 * belong to a group. 683 * @throws NoResponseException if there was no response from the server. 684 * @throws XMPPErrorException if an XMPP exception occurs. 685 * @throws NotLoggedInException If not logged in. 686 * @throws NotConnectedException if the XMPP connection is not connected. 687 * @throws InterruptedException if the calling thread was interrupted. 688 * @since 4.4.0 689 */ 690 public void createItem(BareJid jid, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 691 final XMPPConnection connection = getAuthenticatedConnectionOrThrow(); 692 693 // Create and send roster entry creation packet. 694 RosterPacket rosterPacket = new RosterPacket(); 695 rosterPacket.setType(IQ.Type.set); 696 RosterPacket.Item item = new RosterPacket.Item(jid, name); 697 if (groups != null) { 698 for (String group : groups) { 699 if (group != null && group.trim().length() > 0) { 700 item.addGroupName(group); 701 } 702 } 703 } 704 rosterPacket.addRosterItem(item); 705 connection.sendIqRequestAndWaitForResponse(rosterPacket); 706 } 707 708 /** 709 * Creates a new roster entry and presence subscription. The server will asynchronously 710 * update the roster with the subscription status. 711 * 712 * @param jid the XMPP address of the contact (e.g. johndoe@jabber.org) 713 * @param name the nickname of the user. 714 * @param groups the list of group names the entry will belong to, or <code>null</code> if the 715 * the roster entry won't belong to a group. 716 * @throws NoResponseException if there was no response from the server. 717 * @throws XMPPErrorException if an XMPP exception occurs. 718 * @throws NotLoggedInException If not logged in. 719 * @throws NotConnectedException if the XMPP connection is not connected. 720 * @throws InterruptedException if the calling thread was interrupted. 721 * @since 4.4.0 722 */ 723 public void createItemAndRequestSubscription(BareJid jid, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 724 createItem(jid, name, groups); 725 726 sendSubscriptionRequest(jid); 727 } 728 729 /** 730 * Creates a new pre-approved roster entry and presence subscription. The server will 731 * asynchronously update the roster with the subscription status. 732 * 733 * @param user the user. (e.g. johndoe@jabber.org) 734 * @param name the nickname of the user. 735 * @param groups the list of group names the entry will belong to, or <code>null</code> if the 736 * the roster entry won't belong to a group. 737 * @throws NoResponseException if there was no response from the server. 738 * @throws XMPPErrorException if an XMPP exception occurs. 739 * @throws NotLoggedInException if not logged in. 740 * @throws NotConnectedException if the XMPP connection is not connected. 741 * @throws InterruptedException if the calling thread was interrupted. 742 * @throws FeatureNotSupportedException if pre-approving is not supported. 743 * @since 4.2 744 */ 745 public void preApproveAndCreateEntry(BareJid user, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, FeatureNotSupportedException { 746 preApprove(user); 747 createItemAndRequestSubscription(user, name, groups); 748 } 749 750 /** 751 * Pre-approve user presence subscription. 752 * 753 * @param user the user. (e.g. johndoe@jabber.org) 754 * @throws NotLoggedInException if not logged in. 755 * @throws NotConnectedException if the XMPP connection is not connected. 756 * @throws InterruptedException if the calling thread was interrupted. 757 * @throws FeatureNotSupportedException if pre-approving is not supported. 758 * @since 4.2 759 */ 760 public void preApprove(BareJid user) throws NotLoggedInException, NotConnectedException, InterruptedException, FeatureNotSupportedException { 761 final XMPPConnection connection = connection(); 762 if (!isSubscriptionPreApprovalSupported()) { 763 throw new FeatureNotSupportedException("Pre-approving"); 764 } 765 766 Presence presencePacket = connection.getStanzaFactory().buildPresenceStanza() 767 .ofType(Presence.Type.subscribed) 768 .to(user) 769 .build(); 770 connection.sendStanza(presencePacket); 771 } 772 773 /** 774 * Check for subscription pre-approval support. 775 * 776 * @return true if subscription pre-approval is supported by the server. 777 * @throws NotLoggedInException if not logged in. 778 * @since 4.2 779 */ 780 public boolean isSubscriptionPreApprovalSupported() throws NotLoggedInException { 781 final XMPPConnection connection = getAuthenticatedConnectionOrThrow(); 782 return connection.hasFeature(SubscriptionPreApproval.ELEMENT, SubscriptionPreApproval.NAMESPACE); 783 } 784 785 public void sendSubscriptionRequest(BareJid jid) throws NotLoggedInException, NotConnectedException, InterruptedException { 786 final XMPPConnection connection = getAuthenticatedConnectionOrThrow(); 787 788 // Create a presence subscription packet and send. 789 Presence presencePacket = connection.getStanzaFactory().buildPresenceStanza() 790 .ofType(Presence.Type.subscribe) 791 .to(jid) 792 .build(); 793 connection.sendStanza(presencePacket); 794 } 795 796 /** 797 * Add a subscribe listener, which is invoked on incoming subscription requests and if 798 * {@link SubscriptionMode} is set to {@link SubscriptionMode#manual}. This also sets subscription 799 * mode to {@link SubscriptionMode#manual}. 800 * 801 * @param subscribeListener the subscribe listener to add. 802 * @return <code>true</code> if the listener was not already added. 803 * @since 4.2 804 */ 805 public boolean addSubscribeListener(SubscribeListener subscribeListener) { 806 Objects.requireNonNull(subscribeListener, "SubscribeListener argument must not be null"); 807 if (subscriptionMode != SubscriptionMode.manual) { 808 previousSubscriptionMode = subscriptionMode; 809 subscriptionMode = SubscriptionMode.manual; 810 } 811 return subscribeListeners.add(subscribeListener); 812 } 813 814 /** 815 * Remove a subscribe listener. Also restores the previous subscription mode 816 * state, if the last listener got removed. 817 * 818 * @param subscribeListener TODO javadoc me please 819 * the subscribe listener to remove. 820 * @return <code>true</code> if the listener registered and got removed. 821 * @since 4.2 822 */ 823 public boolean removeSubscribeListener(SubscribeListener subscribeListener) { 824 boolean removed = subscribeListeners.remove(subscribeListener); 825 if (removed && subscribeListeners.isEmpty()) { 826 setSubscriptionMode(previousSubscriptionMode); 827 } 828 return removed; 829 } 830 831 /** 832 * Removes a roster entry from the roster. The roster entry will also be removed from the 833 * unfiled entries or from any roster group where it could belong and will no longer be part 834 * of the roster. Note that this is a synchronous call -- Smack must wait for the server 835 * to send an updated subscription status. 836 * 837 * @param entry a roster entry. 838 * @throws XMPPErrorException if an XMPP error occurs. 839 * @throws NotLoggedInException if not logged in. 840 * @throws NoResponseException SmackException if there was no response from the server. 841 * @throws NotConnectedException if the XMPP connection is not connected. 842 * @throws InterruptedException if the calling thread was interrupted. 843 */ 844 public void removeEntry(RosterEntry entry) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 845 final XMPPConnection connection = getAuthenticatedConnectionOrThrow(); 846 847 // Only remove the entry if it's in the entry list. 848 // The actual removal logic takes place in RosterPacketListenerProcess>>Packet(Packet) 849 if (!entries.containsKey(entry.getJid())) { 850 return; 851 } 852 RosterPacket packet = new RosterPacket(); 853 packet.setType(IQ.Type.set); 854 RosterPacket.Item item = RosterEntry.toRosterItem(entry); 855 // Set the item type as REMOVE so that the server will delete the entry 856 item.setItemType(RosterPacket.ItemType.remove); 857 packet.addRosterItem(item); 858 connection.sendIqRequestAndWaitForResponse(packet); 859 } 860 861 /** 862 * Returns a count of the entries in the roster. 863 * 864 * @return the number of entries in the roster. 865 */ 866 public int getEntryCount() { 867 return getEntries().size(); 868 } 869 870 /** 871 * Add a roster listener and invoke the roster entries with all entries of the roster. 872 * <p> 873 * The method guarantees that the listener is only invoked after 874 * {@link RosterEntries#rosterEntries(Collection)} has been invoked, and that all roster events 875 * that happen while <code>rosterEntries(Collection) </code> is called are queued until the 876 * method returns. 877 * </p> 878 * <p> 879 * This guarantee makes this the ideal method to e.g. populate a UI element with the roster while 880 * installing a {@link RosterListener} to listen for subsequent roster events. 881 * </p> 882 * 883 * @param rosterListener the listener to install 884 * @param rosterEntries the roster entries callback interface 885 * @since 4.1 886 */ 887 public void getEntriesAndAddListener(RosterListener rosterListener, RosterEntries rosterEntries) { 888 Objects.requireNonNull(rosterListener, "listener must not be null"); 889 Objects.requireNonNull(rosterEntries, "rosterEntries must not be null"); 890 891 synchronized (rosterListenersAndEntriesLock) { 892 rosterEntries.rosterEntries(entries.values()); 893 addRosterListener(rosterListener); 894 } 895 } 896 897 /** 898 * Returns a set of all entries in the roster, including entries 899 * that don't belong to any groups. 900 * 901 * @return all entries in the roster. 902 */ 903 public Set<RosterEntry> getEntries() { 904 Set<RosterEntry> allEntries; 905 synchronized (rosterListenersAndEntriesLock) { 906 allEntries = new HashSet<>(entries.size()); 907 for (RosterEntry entry : entries.values()) { 908 allEntries.add(entry); 909 } 910 } 911 return allEntries; 912 } 913 914 /** 915 * Returns a count of the unfiled entries in the roster. An unfiled entry is 916 * an entry that doesn't belong to any groups. 917 * 918 * @return the number of unfiled entries in the roster. 919 */ 920 public int getUnfiledEntryCount() { 921 return unfiledEntries.size(); 922 } 923 924 /** 925 * Returns an unmodifiable set for the unfiled roster entries. An unfiled entry is 926 * an entry that doesn't belong to any groups. 927 * 928 * @return the unfiled roster entries. 929 */ 930 public Set<RosterEntry> getUnfiledEntries() { 931 return Collections.unmodifiableSet(unfiledEntries); 932 } 933 934 /** 935 * Returns the roster entry associated with the given XMPP address or 936 * <code>null</code> if the user is not an entry in the roster. 937 * 938 * @param jid the XMPP address of the user (eg "jsmith@example.com"). The address could be 939 * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource"). 940 * @return the roster entry or <code>null</code> if it does not exist. 941 */ 942 public RosterEntry getEntry(BareJid jid) { 943 if (jid == null) { 944 return null; 945 } 946 return entries.get(jid); 947 } 948 949 /** 950 * Returns true if the specified XMPP address is an entry in the roster. 951 * 952 * @param jid the XMPP address of the user (eg "jsmith@example.com"). The 953 * address must be a bare JID e.g. "domain/resource" or 954 * "user@domain". 955 * @return true if the XMPP address is an entry in the roster. 956 */ 957 public boolean contains(BareJid jid) { 958 return getEntry(jid) != null; 959 } 960 961 /** 962 * Returns the roster group with the specified name, or <code>null</code> if the 963 * group doesn't exist. 964 * 965 * @param name the name of the group. 966 * @return the roster group with the specified name. 967 */ 968 public RosterGroup getGroup(String name) { 969 return groups.get(name); 970 } 971 972 /** 973 * Returns the number of the groups in the roster. 974 * 975 * @return the number of groups in the roster. 976 */ 977 public int getGroupCount() { 978 return groups.size(); 979 } 980 981 /** 982 * Returns an unmodifiable collections of all the roster groups. 983 * 984 * @return an iterator for all roster groups. 985 */ 986 public Collection<RosterGroup> getGroups() { 987 return Collections.unmodifiableCollection(groups.values()); 988 } 989 990 /** 991 * Returns the presence info for a particular user. If the user is offline, or 992 * if no presence data is available (such as when you are not subscribed to the 993 * user's presence updates), unavailable presence will be returned. 994 * 995 * If the user has several presences (one for each resource), then the presence with 996 * highest priority will be returned. If multiple presences have the same priority, 997 * the one with the "most available" presence mode will be returned. In order, 998 * that's {@link org.jivesoftware.smack.packet.Presence.Mode#chat free to chat}, 999 * {@link org.jivesoftware.smack.packet.Presence.Mode#available available}, 1000 * {@link org.jivesoftware.smack.packet.Presence.Mode#away away}, 1001 * {@link org.jivesoftware.smack.packet.Presence.Mode#xa extended away}, and 1002 * {@link org.jivesoftware.smack.packet.Presence.Mode#dnd do not disturb}. 1003 * 1004 * <p> 1005 * Note that presence information is received asynchronously. So, just after logging 1006 * in to the server, presence values for users in the roster may be unavailable 1007 * even if they are actually online. In other words, the value returned by this 1008 * method should only be treated as a snapshot in time, and may not accurately reflect 1009 * other user's presence instant by instant. If you need to track presence over time, 1010 * such as when showing a visual representation of the roster, consider using a 1011 * {@link RosterListener}. 1012 * </p> 1013 * 1014 * @param jid the XMPP address of the user (eg "jsmith@example.com"). The 1015 * address must be a bare JID e.g. "domain/resource" or 1016 * "user@domain". 1017 * @return the user's current presence, or unavailable presence if the user is offline 1018 * or if no presence information is available.. 1019 */ 1020 public Presence getPresence(BareJid jid) { 1021 Map<Resourcepart, Presence> userPresences = getPresencesInternal(jid); 1022 if (userPresences == null) { 1023 Presence presence = synthesizeUnvailablePresence(jid); 1024 return presence; 1025 } 1026 else { 1027 // Find the resource with the highest priority 1028 // Might be changed to use the resource with the highest availability instead. 1029 Presence presence = null; 1030 // This is used in case no available presence is found 1031 Presence unavailable = null; 1032 1033 for (Presence p : userPresences.values()) { 1034 if (!p.isAvailable()) { 1035 unavailable = p; 1036 continue; 1037 } 1038 // Chose presence with highest priority first. 1039 if (presence == null || p.getPriority() > presence.getPriority()) { 1040 presence = p; 1041 } 1042 // If equal priority, choose "most available" by the mode value. 1043 else if (p.getPriority() == presence.getPriority()) { 1044 Presence.Mode pMode = p.getMode(); 1045 // Default to presence mode of available. 1046 if (pMode == null) { 1047 pMode = Presence.Mode.available; 1048 } 1049 Presence.Mode presenceMode = presence.getMode(); 1050 // Default to presence mode of available. 1051 if (presenceMode == null) { 1052 presenceMode = Presence.Mode.available; 1053 } 1054 if (pMode.compareTo(presenceMode) < 0) { 1055 presence = p; 1056 } 1057 } 1058 } 1059 if (presence == null) { 1060 if (unavailable != null) { 1061 return unavailable; 1062 } 1063 else { 1064 presence = synthesizeUnvailablePresence(jid); 1065 return presence; 1066 } 1067 } 1068 else { 1069 return presence; 1070 } 1071 } 1072 } 1073 1074 /** 1075 * Returns the presence info for a particular user's resource, or unavailable presence 1076 * if the user is offline or if no presence information is available, such as 1077 * when you are not subscribed to the user's presence updates. 1078 * 1079 * @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource). 1080 * @return the user's current presence, or unavailable presence if the user is offline 1081 * or if no presence information is available. 1082 */ 1083 public Presence getPresenceResource(FullJid userWithResource) { 1084 BareJid key = userWithResource.asBareJid(); 1085 Resourcepart resource = userWithResource.getResourcepart(); 1086 Map<Resourcepart, Presence> userPresences = getPresencesInternal(key); 1087 if (userPresences == null) { 1088 Presence presence = synthesizeUnvailablePresence(userWithResource); 1089 return presence; 1090 } 1091 else { 1092 Presence presence = userPresences.get(resource); 1093 if (presence == null) { 1094 presence = synthesizeUnvailablePresence(userWithResource); 1095 return presence; 1096 } 1097 else { 1098 return presence; 1099 } 1100 } 1101 } 1102 1103 /** 1104 * Returns a List of Presence objects for all of a user's current presences if no presence information is available, 1105 * such as when you are not subscribed to the user's presence updates. 1106 * 1107 * @param bareJid an XMPP ID, e.g. jdoe@example.com. 1108 * @return a List of Presence objects for all the user's current presences, or an unavailable presence if no 1109 * presence information is available. 1110 */ 1111 public List<Presence> getAllPresences(BareJid bareJid) { 1112 Map<Resourcepart, Presence> userPresences = getPresencesInternal(bareJid); 1113 List<Presence> res; 1114 if (userPresences == null) { 1115 // Create an unavailable presence if none was found 1116 Presence unavailable = synthesizeUnvailablePresence(bareJid); 1117 res = new ArrayList<>(Arrays.asList(unavailable)); 1118 } else { 1119 res = new ArrayList<>(userPresences.values().size()); 1120 for (Presence presence : userPresences.values()) { 1121 res.add(presence); 1122 } 1123 } 1124 return res; 1125 } 1126 1127 /** 1128 * Returns a List of all <b>available</b> Presence Objects for the given bare JID. If there are no available 1129 * presences, then the empty list will be returned. 1130 * 1131 * @param bareJid the bare JID from which the presences should be retrieved. 1132 * @return available presences for the bare JID. 1133 */ 1134 public List<Presence> getAvailablePresences(BareJid bareJid) { 1135 List<Presence> allPresences = getAllPresences(bareJid); 1136 List<Presence> res = new ArrayList<>(allPresences.size()); 1137 for (Presence presence : allPresences) { 1138 if (presence.isAvailable()) { 1139 // No need to clone presence here, getAllPresences already returns clones 1140 res.add(presence); 1141 } 1142 } 1143 return res; 1144 } 1145 1146 /** 1147 * Returns a List of Presence objects for all of a user's current presences 1148 * or an unavailable presence if the user is unavailable (offline) or if no presence 1149 * information is available, such as when you are not subscribed to the user's presence 1150 * updates. 1151 * 1152 * @param jid an XMPP ID, e.g. jdoe@example.com. 1153 * @return a List of Presence objects for all the user's current presences, 1154 * or an unavailable presence if the user is offline or if no presence information 1155 * is available. 1156 */ 1157 public List<Presence> getPresences(BareJid jid) { 1158 List<Presence> res; 1159 Map<Resourcepart, Presence> userPresences = getPresencesInternal(jid); 1160 if (userPresences == null) { 1161 Presence presence = synthesizeUnvailablePresence(jid); 1162 res = Arrays.asList(presence); 1163 } 1164 else { 1165 List<Presence> answer = new ArrayList<>(); 1166 // Used in case no available presence is found 1167 Presence unavailable = null; 1168 for (Presence presence : userPresences.values()) { 1169 if (presence.isAvailable()) { 1170 answer.add(presence); 1171 } 1172 else { 1173 unavailable = presence; 1174 } 1175 } 1176 if (!answer.isEmpty()) { 1177 res = answer; 1178 } 1179 else if (unavailable != null) { 1180 res = Arrays.asList(unavailable); 1181 } 1182 else { 1183 Presence presence = synthesizeUnvailablePresence(jid); 1184 res = Arrays.asList(presence); 1185 } 1186 } 1187 return res; 1188 } 1189 1190 /** 1191 * Check if the given JID is subscribed to the user's presence. 1192 * <p> 1193 * If the JID is subscribed to the user's presence then it is allowed to see the presence and 1194 * will get notified about presence changes. Also returns true, if the JID is the service 1195 * name of the XMPP connection (the "XMPP domain"), i.e. the XMPP service is treated like 1196 * having an implicit subscription to the users presence. 1197 * </p> 1198 * Note that if the roster is not loaded, then this method will always return false. 1199 * 1200 * @param jid TODO javadoc me please 1201 * @return true if the given JID is allowed to see the users presence. 1202 * @since 4.1 1203 */ 1204 public boolean isSubscribedToMyPresence(Jid jid) { 1205 if (jid == null) { 1206 return false; 1207 } 1208 BareJid bareJid = jid.asBareJid(); 1209 if (connection().getXMPPServiceDomain().equals(bareJid)) { 1210 return true; 1211 } 1212 RosterEntry entry = getEntry(bareJid); 1213 if (entry == null) { 1214 return false; 1215 } 1216 return entry.canSeeMyPresence(); 1217 } 1218 1219 /** 1220 * Check if the XMPP entity this roster belongs to is subscribed to the presence of the given JID. 1221 * 1222 * @param jid the jid to check. 1223 * @return <code>true</code> if we are subscribed to the presence of the given jid. 1224 * @since 4.2 1225 */ 1226 public boolean iAmSubscribedTo(Jid jid) { 1227 if (jid == null) { 1228 return false; 1229 } 1230 BareJid bareJid = jid.asBareJid(); 1231 RosterEntry entry = getEntry(bareJid); 1232 if (entry == null) { 1233 return false; 1234 } 1235 return entry.canSeeHisPresence(); 1236 } 1237 1238 /** 1239 * Sets if the roster will be loaded from the server when logging in for newly created instances 1240 * of {@link Roster}. 1241 * 1242 * @param rosterLoadedAtLoginDefault if the roster will be loaded from the server when logging in. 1243 * @see #setRosterLoadedAtLogin(boolean) 1244 * @since 4.1.7 1245 */ 1246 public static void setRosterLoadedAtLoginDefault(boolean rosterLoadedAtLoginDefault) { 1247 Roster.rosterLoadedAtLoginDefault = rosterLoadedAtLoginDefault; 1248 } 1249 1250 /** 1251 * Sets if the roster will be loaded from the server when logging in. This 1252 * is the common behaviour for clients but sometimes clients may want to differ this 1253 * or just never do it if not interested in rosters. 1254 * 1255 * @param rosterLoadedAtLogin if the roster will be loaded from the server when logging in. 1256 */ 1257 public void setRosterLoadedAtLogin(boolean rosterLoadedAtLogin) { 1258 this.rosterLoadedAtLogin = rosterLoadedAtLogin; 1259 } 1260 1261 /** 1262 * Returns true if the roster will be loaded from the server when logging in. This 1263 * is the common behavior for clients but sometimes clients may want to differ this 1264 * or just never do it if not interested in rosters. 1265 * 1266 * @return true if the roster will be loaded from the server when logging in. 1267 * @see <a href="http://xmpp.org/rfcs/rfc6121.html#roster-login">RFC 6121 2.2 - Retrieving the Roster on Login</a> 1268 */ 1269 public boolean isRosterLoadedAtLogin() { 1270 return rosterLoadedAtLogin; 1271 } 1272 1273 RosterStore getRosterStore() { 1274 return rosterStore; 1275 } 1276 1277 /** 1278 * Changes the presence of available contacts offline by simulating an unavailable 1279 * presence sent from the server. 1280 */ 1281 private void setOfflinePresences() { 1282 outerloop: for (Jid user : presenceMap.keySet()) { 1283 Map<Resourcepart, Presence> resources = presenceMap.get(user); 1284 if (resources != null) { 1285 for (Resourcepart resource : resources.keySet()) { 1286 PresenceBuilder presenceBuilder = StanzaBuilder.buildPresence() 1287 .ofType(Presence.Type.unavailable); 1288 EntityBareJid bareUserJid = user.asEntityBareJidIfPossible(); 1289 if (bareUserJid == null) { 1290 LOGGER.warning("Can not transform user JID to bare JID: '" + user + "'"); 1291 continue; 1292 } 1293 presenceBuilder.from(JidCreate.fullFrom(bareUserJid, resource)); 1294 try { 1295 presencePacketListener.processStanza(presenceBuilder.build()); 1296 } 1297 catch (NotConnectedException e) { 1298 throw new IllegalStateException( 1299 "presencePacketListener should never throw a NotConnectedException when processStanza is called with a presence of type unavailable", 1300 e); 1301 } 1302 catch (InterruptedException e) { 1303 break outerloop; 1304 } 1305 } 1306 } 1307 } 1308 } 1309 1310 /** 1311 * Changes the presence of available contacts offline by simulating an unavailable 1312 * presence sent from the server. After a disconnection, every Presence is set 1313 * to offline. 1314 */ 1315 private void setOfflinePresencesAndResetLoaded() { 1316 setOfflinePresences(); 1317 rosterState = RosterState.uninitialized; 1318 } 1319 1320 /** 1321 * Fires roster changed event to roster listeners indicating that the 1322 * specified collections of contacts have been added, updated or deleted 1323 * from the roster. 1324 * 1325 * @param addedEntries the collection of address of the added contacts. 1326 * @param updatedEntries the collection of address of the updated contacts. 1327 * @param deletedEntries the collection of address of the deleted contacts. 1328 */ 1329 private void fireRosterChangedEvent(final Collection<Jid> addedEntries, final Collection<Jid> updatedEntries, 1330 final Collection<Jid> deletedEntries) { 1331 synchronized (rosterListenersAndEntriesLock) { 1332 for (RosterListener listener : rosterListeners) { 1333 if (!addedEntries.isEmpty()) { 1334 listener.entriesAdded(addedEntries); 1335 } 1336 if (!updatedEntries.isEmpty()) { 1337 listener.entriesUpdated(updatedEntries); 1338 } 1339 if (!deletedEntries.isEmpty()) { 1340 listener.entriesDeleted(deletedEntries); 1341 } 1342 } 1343 } 1344 } 1345 1346 /** 1347 * Fires roster presence changed event to roster listeners. 1348 * 1349 * @param presence the presence change. 1350 */ 1351 private void fireRosterPresenceEvent(final Presence presence) { 1352 synchronized (rosterListenersAndEntriesLock) { 1353 for (RosterListener listener : rosterListeners) { 1354 listener.presenceChanged(presence); 1355 } 1356 } 1357 } 1358 1359 private void addUpdateEntry(Collection<Jid> addedEntries, Collection<Jid> updatedEntries, 1360 Collection<Jid> unchangedEntries, RosterPacket.Item item, RosterEntry entry) { 1361 RosterEntry oldEntry; 1362 synchronized (rosterListenersAndEntriesLock) { 1363 oldEntry = entries.put(item.getJid(), entry); 1364 } 1365 if (oldEntry == null) { 1366 BareJid jid = item.getJid(); 1367 addedEntries.add(jid); 1368 // Move the eventually existing presences from nonRosterPresenceMap to presenceMap. 1369 move(jid, nonRosterPresenceMap, presenceMap); 1370 } 1371 else { 1372 RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry); 1373 if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) { 1374 updatedEntries.add(item.getJid()); 1375 oldEntry.updateItem(item); 1376 } else { 1377 // Record the entry as unchanged, so that it doesn't end up as deleted entry 1378 unchangedEntries.add(item.getJid()); 1379 } 1380 } 1381 1382 // Mark the entry as unfiled if it does not belong to any groups. 1383 if (item.getGroupNames().isEmpty()) { 1384 unfiledEntries.add(entry); 1385 } 1386 else { 1387 unfiledEntries.remove(entry); 1388 } 1389 1390 // Add the entry/user to the groups 1391 List<String> newGroupNames = new ArrayList<>(); 1392 for (String groupName : item.getGroupNames()) { 1393 // Add the group name to the list. 1394 newGroupNames.add(groupName); 1395 1396 // Add the entry to the group. 1397 RosterGroup group = getGroup(groupName); 1398 if (group == null) { 1399 group = createGroup(groupName); 1400 groups.put(groupName, group); 1401 } 1402 // Add the entry. 1403 group.addEntryLocal(entry); 1404 } 1405 1406 // Remove user from the remaining groups. 1407 List<String> oldGroupNames = new ArrayList<>(); 1408 for (RosterGroup group : getGroups()) { 1409 oldGroupNames.add(group.getName()); 1410 } 1411 oldGroupNames.removeAll(newGroupNames); 1412 1413 for (String groupName : oldGroupNames) { 1414 RosterGroup group = getGroup(groupName); 1415 group.removeEntryLocal(entry); 1416 if (group.getEntryCount() == 0) { 1417 groups.remove(groupName); 1418 } 1419 } 1420 } 1421 1422 private void deleteEntry(Collection<Jid> deletedEntries, RosterEntry entry) { 1423 BareJid user = entry.getJid(); 1424 entries.remove(user); 1425 unfiledEntries.remove(entry); 1426 // Move the presences from the presenceMap to the nonRosterPresenceMap. 1427 move(user, presenceMap, nonRosterPresenceMap); 1428 deletedEntries.add(user); 1429 1430 for (Map.Entry<String, RosterGroup> e : groups.entrySet()) { 1431 RosterGroup group = e.getValue(); 1432 group.removeEntryLocal(entry); 1433 if (group.getEntryCount() == 0) { 1434 groups.remove(e.getKey()); 1435 } 1436 } 1437 } 1438 1439 /** 1440 * Removes all the groups with no entries. 1441 * 1442 * This is used by {@link RosterPushListener} and {@link RosterResultListener} to 1443 * cleanup groups after removing contacts. 1444 */ 1445 private void removeEmptyGroups() { 1446 // We have to do this because RosterGroup.removeEntry removes the entry immediately 1447 // (locally) and the group could remain empty. 1448 // TODO Check the performance/logic for rosters with large number of groups 1449 for (RosterGroup group : getGroups()) { 1450 if (group.getEntryCount() == 0) { 1451 groups.remove(group.getName()); 1452 } 1453 } 1454 } 1455 1456 /** 1457 * Move presences from 'entity' from one presence map to another. 1458 * 1459 * @param entity the entity 1460 * @param from the map to move presences from 1461 * @param to the map to move presences to 1462 */ 1463 private static void move(BareJid entity, Map<BareJid, Map<Resourcepart, Presence>> from, Map<BareJid, Map<Resourcepart, Presence>> to) { 1464 Map<Resourcepart, Presence> presences = from.remove(entity); 1465 if (presences != null && !presences.isEmpty()) { 1466 to.put(entity, presences); 1467 } 1468 } 1469 1470 /** 1471 * Ignore ItemTypes as of RFC 6121, 2.1.2.5. 1472 * 1473 * This is used by {@link RosterPushListener} and {@link RosterResultListener}. 1474 * */ 1475 private static boolean hasValidSubscriptionType(RosterPacket.Item item) { 1476 switch (item.getItemType()) { 1477 case none: 1478 case from: 1479 case to: 1480 case both: 1481 return true; 1482 default: 1483 return false; 1484 } 1485 } 1486 1487 private static Presence synthesizeUnvailablePresence(Jid from) { 1488 return StanzaBuilder.buildPresence() 1489 .ofType(Presence.Type.unavailable) 1490 .from(from) 1491 .build(); 1492 } 1493 1494 /** 1495 * Check if the server supports roster versioning. 1496 * 1497 * @return true if the server supports roster versioning, false otherwise. 1498 */ 1499 public boolean isRosterVersioningSupported() { 1500 return connection().hasFeature(RosterVer.ELEMENT, RosterVer.NAMESPACE); 1501 } 1502 1503 /** 1504 * An enumeration for the subscription mode options. 1505 */ 1506 public enum SubscriptionMode { 1507 1508 /** 1509 * Automatically accept all subscription and unsubscription requests. 1510 * This is suitable for simple clients. More complex clients will 1511 * likely wish to handle subscription requests manually. 1512 */ 1513 accept_all, 1514 1515 /** 1516 * Automatically reject all subscription requests. This is the default mode. 1517 */ 1518 reject_all, 1519 1520 /** 1521 * Subscription requests are ignored, which means they must be manually 1522 * processed by registering a listener for presence packets and then looking 1523 * for any presence requests that have the type Presence.Type.SUBSCRIBE or 1524 * Presence.Type.UNSUBSCRIBE. 1525 */ 1526 manual 1527 } 1528 1529 /** 1530 * Listens for all presence packets and processes them. 1531 */ 1532 private class PresencePacketListener implements StanzaListener { 1533 1534 @Override 1535 public void processStanza(Stanza packet) throws NotConnectedException, InterruptedException { 1536 // Try to ensure that the roster is loaded when processing presence stanzas. While the 1537 // presence listener is synchronous, the roster result listener is not, which means that 1538 // the presence listener may be invoked with a not yet loaded roster. 1539 if (rosterState == RosterState.loading) { 1540 try { 1541 waitUntilLoaded(); 1542 } 1543 catch (InterruptedException e) { 1544 LOGGER.log(Level.INFO, "Presence listener was interrupted", e); 1545 1546 } 1547 } 1548 if (!isLoaded() && rosterLoadedAtLogin) { 1549 LOGGER.warning("Roster not loaded while processing " + packet); 1550 } 1551 final Presence presence = (Presence) packet; 1552 final Jid from = presence.getFrom(); 1553 1554 final BareJid key; 1555 if (from != null) { 1556 key = from.asBareJid(); 1557 } else { 1558 XMPPConnection connection = connection(); 1559 if (connection == null) { 1560 LOGGER.finest("Connection was null while trying to handle exotic presence stanza: " + presence); 1561 return; 1562 } 1563 // Assume the presence come "from the users account on the server" since no from was set (RFC 6120 § 1564 // 8.1.2.1 4.). Note that getUser() may return null, but should never return null in this case as where 1565 // connected. 1566 EntityFullJid myJid = connection.getUser(); 1567 if (myJid == null) { 1568 LOGGER.info( 1569 "Connection had no local address in Roster's presence listener." 1570 + " Possibly we received a presence without from before being authenticated." 1571 + " Presence: " + presence); 1572 return; 1573 } 1574 LOGGER.info("Exotic presence stanza without from received: " + presence); 1575 key = myJid.asBareJid(); 1576 } 1577 1578 asyncButOrdered.performAsyncButOrdered(key, new Runnable() { 1579 @Override 1580 public void run() { 1581 Resourcepart fromResource = Resourcepart.EMPTY; 1582 BareJid bareFrom = null; 1583 FullJid fullFrom = null; 1584 if (from != null) { 1585 fromResource = from.getResourceOrNull(); 1586 if (fromResource == null) { 1587 fromResource = Resourcepart.EMPTY; 1588 bareFrom = from.asBareJid(); 1589 } 1590 else { 1591 fullFrom = from.asFullJidIfPossible(); 1592 // We know that this must be a full JID in this case. 1593 assert fullFrom != null; 1594 } 1595 } 1596 Map<Resourcepart, Presence> userPresences; 1597 // If an "available" presence, add it to the presence map. Each presence 1598 // map will hold for a particular user a map with the presence 1599 // packets saved for each resource. 1600 switch (presence.getType()) { 1601 case available: 1602 // Get the user presence map 1603 userPresences = getOrCreatePresencesInternal(key); 1604 // See if an offline presence was being stored in the map. If so, remove 1605 // it since we now have an online presence. 1606 userPresences.remove(Resourcepart.EMPTY); 1607 // Add the new presence, using the resources as a key. 1608 userPresences.put(fromResource, presence); 1609 // If the user is in the roster, fire an event. 1610 if (contains(key)) { 1611 fireRosterPresenceEvent(presence); 1612 } 1613 for (PresenceEventListener presenceEventListener : presenceEventListeners) { 1614 presenceEventListener.presenceAvailable(fullFrom, presence); 1615 } 1616 break; 1617 // If an "unavailable" packet. 1618 case unavailable: 1619 // If no resource, this is likely an offline presence as part of 1620 // a roster presence flood. In that case, we store it. 1621 userPresences = getOrCreatePresencesInternal(key); 1622 if (from.hasNoResource()) { 1623 // Get the user presence map 1624 userPresences.put(Resourcepart.EMPTY, presence); 1625 } 1626 // Otherwise, this is a normal offline presence. 1627 else { 1628 // Store the offline presence, as it may include extra information 1629 // such as the user being on vacation. 1630 userPresences.put(fromResource, presence); 1631 } 1632 // If the user is in the roster, fire an event. 1633 if (contains(key)) { 1634 fireRosterPresenceEvent(presence); 1635 } 1636 1637 // Ensure that 'from' is a full JID before invoking the presence unavailable 1638 // listeners. Usually unavailable presences always have a resourcepart, i.e. are 1639 // full JIDs, but RFC 6121 § 4.5.4 has an implementation note that unavailable 1640 // presences from a bare JID SHOULD be treated as applying to all resources. I don't 1641 // think any client or server ever implemented that, I do think that this 1642 // implementation note is a terrible idea since it adds another corner case in 1643 // client code, instead of just having the invariant 1644 // "unavailable presences are always from the full JID". 1645 if (fullFrom != null) { 1646 for (PresenceEventListener presenceEventListener : presenceEventListeners) { 1647 presenceEventListener.presenceUnavailable(fullFrom, presence); 1648 } 1649 } else { 1650 LOGGER.fine("Unavailable presence from bare JID: " + presence); 1651 } 1652 1653 break; 1654 // Error presence packets from a bare JID mean we invalidate all existing 1655 // presence info for the user. 1656 case error: 1657 // No need to act on error presences send without from, i.e. 1658 // directly send from the users XMPP service, or where the from 1659 // address is not a bare JID 1660 if (from == null || !from.isEntityBareJid()) { 1661 break; 1662 } 1663 userPresences = getOrCreatePresencesInternal(key); 1664 // Any other presence data is invalidated by the error packet. 1665 userPresences.clear(); 1666 1667 // Set the new presence using the empty resource as a key. 1668 userPresences.put(Resourcepart.EMPTY, presence); 1669 // If the user is in the roster, fire an event. 1670 if (contains(key)) { 1671 fireRosterPresenceEvent(presence); 1672 } 1673 for (PresenceEventListener presenceEventListener : presenceEventListeners) { 1674 presenceEventListener.presenceError(from, presence); 1675 } 1676 break; 1677 case subscribed: 1678 for (PresenceEventListener presenceEventListener : presenceEventListeners) { 1679 presenceEventListener.presenceSubscribed(bareFrom, presence); 1680 } 1681 break; 1682 case unsubscribed: 1683 for (PresenceEventListener presenceEventListener : presenceEventListeners) { 1684 presenceEventListener.presenceUnsubscribed(bareFrom, presence); 1685 } 1686 break; 1687 default: 1688 break; 1689 } 1690 } 1691 }); 1692 } 1693 } 1694 1695 /** 1696 * Handles Roster results as described in <a href="https://tools.ietf.org/html/rfc6121#section-2.1.4">RFC 6121 2.1.4</a>. 1697 */ 1698 private class RosterResultListener implements SuccessCallback<IQ> { 1699 1700 @Override 1701 public void onSuccess(IQ packet) { 1702 final XMPPConnection connection = connection(); 1703 LOGGER.log(Level.FINE, "RosterResultListener received {0}", packet); 1704 Collection<Jid> addedEntries = new ArrayList<>(); 1705 Collection<Jid> updatedEntries = new ArrayList<>(); 1706 Collection<Jid> deletedEntries = new ArrayList<>(); 1707 Collection<Jid> unchangedEntries = new ArrayList<>(); 1708 1709 if (packet instanceof RosterPacket) { 1710 // Non-empty roster result. This stanza contains all the roster elements. 1711 RosterPacket rosterPacket = (RosterPacket) packet; 1712 1713 // Ignore items without valid subscription type 1714 ArrayList<Item> validItems = new ArrayList<>(); 1715 for (RosterPacket.Item item : rosterPacket.getRosterItems()) { 1716 if (hasValidSubscriptionType(item)) { 1717 validItems.add(item); 1718 } 1719 } 1720 1721 for (RosterPacket.Item item : validItems) { 1722 RosterEntry entry = new RosterEntry(item, Roster.this, connection); 1723 addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry); 1724 } 1725 1726 // Delete all entries which where not added or updated 1727 Set<Jid> toDelete = new HashSet<>(); 1728 for (RosterEntry entry : entries.values()) { 1729 toDelete.add(entry.getJid()); 1730 } 1731 toDelete.removeAll(addedEntries); 1732 toDelete.removeAll(updatedEntries); 1733 toDelete.removeAll(unchangedEntries); 1734 for (Jid user : toDelete) { 1735 deleteEntry(deletedEntries, entries.get(user)); 1736 } 1737 1738 if (rosterStore != null) { 1739 String version = rosterPacket.getVersion(); 1740 rosterStore.resetEntries(validItems, version); 1741 } 1742 1743 removeEmptyGroups(); 1744 } 1745 else { 1746 // Empty roster result as defined in RFC6121 2.6.3. An empty roster result basically 1747 // means that rosterver was used and the roster hasn't changed (much) since the 1748 // version we presented the server. So we simply load the roster from the store and 1749 // await possible further roster pushes. 1750 List<RosterPacket.Item> storedItems = rosterStore.getEntries(); 1751 if (storedItems == null) { 1752 // The roster store was corrupted. Reset the store and reload the roster without using a roster version. 1753 rosterStore.resetStore(); 1754 try { 1755 reload(); 1756 } catch (NotLoggedInException | NotConnectedException 1757 | InterruptedException e) { 1758 LOGGER.log(Level.FINE, 1759 "Exception while trying to load the roster after the roster store was corrupted", 1760 e); 1761 } 1762 return; 1763 } 1764 for (RosterPacket.Item item : storedItems) { 1765 RosterEntry entry = new RosterEntry(item, Roster.this, connection); 1766 addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry); 1767 } 1768 } 1769 1770 rosterState = RosterState.loaded; 1771 synchronized (Roster.this) { 1772 Roster.this.notifyAll(); 1773 } 1774 // Fire event for roster listeners. 1775 fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); 1776 1777 // Call the roster loaded listeners after the roster events have been fired. This is 1778 // important because the user may call getEntriesAndAddListener() in onRosterLoaded(), 1779 // and if the order would be the other way around, the roster listener added by 1780 // getEntriesAndAddListener() would be invoked with information that was already 1781 // available at the time getEntriesAndAddListener() was called. 1782 try { 1783 synchronized (rosterLoadedListeners) { 1784 for (RosterLoadedListener rosterLoadedListener : rosterLoadedListeners) { 1785 rosterLoadedListener.onRosterLoaded(Roster.this); 1786 } 1787 } 1788 } 1789 catch (Exception e) { 1790 LOGGER.log(Level.WARNING, "RosterLoadedListener threw exception", e); 1791 } 1792 } 1793 } 1794 1795 /** 1796 * Listens for all roster pushes and processes them. 1797 */ 1798 private final class RosterPushListener extends AbstractIqRequestHandler { 1799 1800 private RosterPushListener() { 1801 super(RosterPacket.ELEMENT, RosterPacket.NAMESPACE, IQ.Type.set, Mode.sync); 1802 } 1803 1804 @Override 1805 public IQ handleIQRequest(IQ iqRequest) { 1806 final XMPPConnection connection = connection(); 1807 RosterPacket rosterPacket = (RosterPacket) iqRequest; 1808 1809 EntityFullJid ourFullJid = connection.getUser(); 1810 if (ourFullJid == null) { 1811 LOGGER.warning("Ignoring roster push " + iqRequest + " while " + connection 1812 + " has no bound resource. This may be a server bug."); 1813 return null; 1814 } 1815 1816 // Roster push (RFC 6121, 2.1.6) 1817 // A roster push with a non-empty from not matching our address MUST be ignored 1818 EntityBareJid ourBareJid = ourFullJid.asEntityBareJid(); 1819 Jid from = rosterPacket.getFrom(); 1820 if (from != null) { 1821 if (from.equals(ourFullJid)) { 1822 // Since RFC 6121 roster pushes are no longer allowed to 1823 // origin from the full JID as it was the case with RFC 1824 // 3921. Log a warning an continue processing the push. 1825 // See also SMACK-773. 1826 LOGGER.warning( 1827 "Received roster push from full JID. This behavior is since RFC 6121 not longer standard compliant. " 1828 + "Please ask your server vendor to fix this and comply to RFC 6121 § 2.1.6. IQ roster push stanza: " 1829 + iqRequest); 1830 } else if (!from.equals(ourBareJid)) { 1831 LOGGER.warning("Ignoring roster push with a non matching 'from' ourJid='" + ourBareJid + "' from='" 1832 + from + "'"); 1833 return IQ.createErrorResponse(iqRequest, Condition.service_unavailable); 1834 } 1835 } 1836 1837 // A roster push must contain exactly one entry 1838 Collection<Item> items = rosterPacket.getRosterItems(); 1839 if (items.size() != 1) { 1840 LOGGER.warning("Ignoring roster push with not exactly one entry. size=" + items.size()); 1841 return IQ.createErrorResponse(iqRequest, Condition.bad_request); 1842 } 1843 1844 Collection<Jid> addedEntries = new ArrayList<>(); 1845 Collection<Jid> updatedEntries = new ArrayList<>(); 1846 Collection<Jid> deletedEntries = new ArrayList<>(); 1847 Collection<Jid> unchangedEntries = new ArrayList<>(); 1848 1849 // We assured above that the size of items is exactly 1, therefore we are able to 1850 // safely retrieve this single item here. 1851 Item item = items.iterator().next(); 1852 RosterEntry entry = new RosterEntry(item, Roster.this, connection); 1853 String version = rosterPacket.getVersion(); 1854 1855 if (item.getItemType().equals(RosterPacket.ItemType.remove)) { 1856 deleteEntry(deletedEntries, entry); 1857 if (rosterStore != null) { 1858 rosterStore.removeEntry(entry.getJid(), version); 1859 } 1860 } 1861 else if (hasValidSubscriptionType(item)) { 1862 addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry); 1863 if (rosterStore != null) { 1864 rosterStore.addEntry(item, version); 1865 } 1866 } 1867 1868 removeEmptyGroups(); 1869 1870 // Fire event for roster listeners. 1871 fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); 1872 1873 return IQ.createResultIQ(rosterPacket); 1874 } 1875 } 1876 1877 /** 1878 * Set the default maximum size of the non-Roster presence map. 1879 * <p> 1880 * The roster will only store this many presence entries for entities non in the Roster. The 1881 * default is {@value #INITIAL_DEFAULT_NON_ROSTER_PRESENCE_MAP_SIZE}. 1882 * </p> 1883 * 1884 * @param maximumSize the maximum size 1885 * @since 4.2 1886 */ 1887 public static void setDefaultNonRosterPresenceMapMaxSize(int maximumSize) { 1888 defaultNonRosterPresenceMapMaxSize = maximumSize; 1889 } 1890 1891 /** 1892 * Set the maximum size of the non-Roster presence map. 1893 * 1894 * @param maximumSize TODO javadoc me please 1895 * @since 4.2 1896 * @see #setDefaultNonRosterPresenceMapMaxSize(int) 1897 */ 1898 public void setNonRosterPresenceMapMaxSize(int maximumSize) { 1899 nonRosterPresenceMap.setMaxCacheSize(maximumSize); 1900 } 1901 1902}