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