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