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