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