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