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