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