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