001/** 002 * 003 * Copyright 2003-2007 Jive Software. 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.Locale; 028import java.util.Map; 029import java.util.Map.Entry; 030import java.util.Set; 031import java.util.WeakHashMap; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.CopyOnWriteArraySet; 034import java.util.logging.Level; 035import java.util.logging.Logger; 036 037import org.jivesoftware.smack.AbstractConnectionClosedListener; 038import org.jivesoftware.smack.ConnectionCreationListener; 039import org.jivesoftware.smack.ExceptionCallback; 040import org.jivesoftware.smack.Manager; 041import org.jivesoftware.smack.StanzaListener; 042import org.jivesoftware.smack.SmackException; 043import org.jivesoftware.smack.XMPPConnection; 044import org.jivesoftware.smack.SmackException.NoResponseException; 045import org.jivesoftware.smack.SmackException.NotConnectedException; 046import org.jivesoftware.smack.SmackException.NotLoggedInException; 047import org.jivesoftware.smack.XMPPConnectionRegistry; 048import org.jivesoftware.smack.XMPPException.XMPPErrorException; 049import org.jivesoftware.smack.filter.StanzaFilter; 050import org.jivesoftware.smack.filter.StanzaTypeFilter; 051import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; 052import org.jivesoftware.smack.packet.IQ; 053import org.jivesoftware.smack.packet.IQ.Type; 054import org.jivesoftware.smack.packet.Stanza; 055import org.jivesoftware.smack.packet.Presence; 056import org.jivesoftware.smack.packet.XMPPError; 057import org.jivesoftware.smack.packet.XMPPError.Condition; 058import org.jivesoftware.smack.roster.packet.RosterPacket; 059import org.jivesoftware.smack.roster.packet.RosterVer; 060import org.jivesoftware.smack.roster.packet.RosterPacket.Item; 061import org.jivesoftware.smack.roster.rosterstore.RosterStore; 062import org.jivesoftware.smack.util.Objects; 063import org.jxmpp.util.XmppStringUtils; 064 065/** 066 * Represents a user's roster, which is the collection of users a person receives 067 * presence updates for. Roster items are categorized into groups for easier management.<p> 068 * <p/> 069 * Others users may attempt to subscribe to this user using a subscription request. Three 070 * modes are supported for handling these requests: <ul> 071 * <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li> 072 * <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li> 073 * <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li> 074 * </ul> 075 * 076 * @author Matt Tucker 077 * @see #getInstanceFor(XMPPConnection) 078 */ 079public class Roster extends Manager { 080 081 private static final Logger LOGGER = Logger.getLogger(Roster.class.getName()); 082 083 static { 084 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 085 @Override 086 public void connectionCreated(XMPPConnection connection) { 087 getInstanceFor(connection); 088 } 089 }); 090 } 091 092 private static final Map<XMPPConnection, Roster> INSTANCES = new WeakHashMap<>(); 093 094 /** 095 * Returns the roster for the user. 096 * <p> 097 * This method will never return <code>null</code>, instead if the user has not yet logged into 098 * the server or is logged in anonymously all modifying methods of the returned roster object 099 * like {@link Roster#createEntry(String, String, String[])}, 100 * {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing 101 * {@link RosterListener}s will throw an IllegalStateException. 102 * 103 * @return the user's roster. 104 * @throws IllegalStateException if the connection is anonymous 105 */ 106 public static synchronized Roster getInstanceFor(XMPPConnection connection) { 107 Roster roster = INSTANCES.get(connection); 108 if (roster == null) { 109 roster = new Roster(connection); 110 INSTANCES.put(connection, roster); 111 } 112 return roster; 113 } 114 115 private static final StanzaFilter PRESENCE_PACKET_FILTER = StanzaTypeFilter.PRESENCE; 116 117 private static boolean rosterLoadedAtLoginDefault = true; 118 119 /** 120 * The default subscription processing mode to use when a Roster is created. By default 121 * all subscription requests are automatically accepted. 122 */ 123 private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.accept_all; 124 125 private RosterStore rosterStore; 126 private final Map<String, RosterGroup> groups = new ConcurrentHashMap<String, RosterGroup>(); 127 128 /** 129 * Concurrent hash map from JID to its roster entry. 130 */ 131 private final Map<String,RosterEntry> entries = new ConcurrentHashMap<String,RosterEntry>(); 132 133 private final Set<RosterEntry> unfiledEntries = new CopyOnWriteArraySet<>(); 134 private final Set<RosterListener> rosterListeners = new LinkedHashSet<>(); 135 private final Map<String, Map<String, Presence>> presenceMap = new ConcurrentHashMap<String, Map<String, Presence>>(); 136 137 /** 138 * Listeners called when the Roster was loaded. 139 */ 140 private final Set<RosterLoadedListener> rosterLoadedListeners = new LinkedHashSet<>(); 141 142 /** 143 * Mutually exclude roster listener invocation and changing the {@link entries} map. Also used 144 * to synchronize access to either the roster listeners or the entries map. 145 */ 146 private final Object rosterListenersAndEntriesLock = new Object(); 147 148 private enum RosterState { 149 uninitialized, 150 loading, 151 loaded, 152 } 153 154 /** 155 * The current state of the roster. 156 */ 157 private RosterState rosterState = RosterState.uninitialized; 158 159 private final PresencePacketListener presencePacketListener = new PresencePacketListener(); 160 161 /** 162 * 163 */ 164 private boolean rosterLoadedAtLogin = rosterLoadedAtLoginDefault; 165 166 private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode(); 167 168 /** 169 * Returns the default subscription processing mode to use when a new Roster is created. The 170 * subscription processing mode dictates what action Smack will take when subscription 171 * requests from other users are made. The default subscription mode 172 * is {@link SubscriptionMode#accept_all}. 173 * 174 * @return the default subscription mode to use for new Rosters 175 */ 176 public static SubscriptionMode getDefaultSubscriptionMode() { 177 return defaultSubscriptionMode; 178 } 179 180 /** 181 * Sets the default subscription processing mode to use when a new Roster is created. The 182 * subscription processing mode dictates what action Smack will take when subscription 183 * requests from other users are made. The default subscription mode 184 * is {@link SubscriptionMode#accept_all}. 185 * 186 * @param subscriptionMode the default subscription mode to use for new Rosters. 187 */ 188 public static void setDefaultSubscriptionMode(SubscriptionMode subscriptionMode) { 189 defaultSubscriptionMode = subscriptionMode; 190 } 191 192 /** 193 * Creates a new roster. 194 * 195 * @param connection an XMPP connection. 196 */ 197 private Roster(final XMPPConnection connection) { 198 super(connection); 199 200 // Note that we use sync packet listeners because RosterListeners should be invoked in the same order as the 201 // roster stanzas arrive. 202 // Listen for any roster packets. 203 connection.registerIQRequestHandler(new RosterPushListener()); 204 // Listen for any presence packets. 205 connection.addSyncStanzaListener(presencePacketListener, PRESENCE_PACKET_FILTER); 206 207 // Listen for connection events 208 connection.addConnectionListener(new AbstractConnectionClosedListener() { 209 210 @Override 211 public void authenticated(XMPPConnection connection, boolean resumed) { 212 // Anonymous users can't have a roster, but it is possible that a Roster instance is 213 // retrieved if getRoster() is called *before* connect(). So we have to check here 214 // again if it's an anonymous connection. 215 if (connection.isAnonymous()) 216 return; 217 if (!isRosterLoadedAtLogin()) 218 return; 219 // We are done here if the connection was resumed 220 if (resumed) { 221 return; 222 } 223 try { 224 Roster.this.reload(); 225 } 226 catch (SmackException e) { 227 LOGGER.log(Level.SEVERE, "Could not reload Roster", e); 228 return; 229 } 230 } 231 232 @Override 233 public void connectionTerminated() { 234 // Changes the presence available contacts to unavailable 235 setOfflinePresencesAndResetLoaded(); 236 } 237 238 }); 239 // If the connection is already established, call reload 240 if (connection.isAuthenticated()) { 241 try { 242 reload(); 243 } 244 catch (SmackException e) { 245 LOGGER.log(Level.SEVERE, "Could not reload Roster", e); 246 } 247 } 248 } 249 250 /** 251 * Returns the subscription processing mode, which dictates what action 252 * Smack will take when subscription requests from other users are made. 253 * The default subscription mode is {@link SubscriptionMode#accept_all}.<p> 254 * <p/> 255 * If using the manual mode, a PacketListener should be registered that 256 * listens for Presence packets that have a type of 257 * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. 258 * 259 * @return the subscription mode. 260 */ 261 public SubscriptionMode getSubscriptionMode() { 262 return subscriptionMode; 263 } 264 265 /** 266 * Sets the subscription processing mode, which dictates what action 267 * Smack will take when subscription requests from other users are made. 268 * The default subscription mode is {@link SubscriptionMode#accept_all}.<p> 269 * <p/> 270 * If using the manual mode, a PacketListener should be registered that 271 * listens for Presence packets that have a type of 272 * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. 273 * 274 * @param subscriptionMode the subscription mode. 275 */ 276 public void setSubscriptionMode(SubscriptionMode subscriptionMode) { 277 this.subscriptionMode = subscriptionMode; 278 } 279 280 /** 281 * Reloads the entire roster from the server. This is an asynchronous operation, 282 * which means the method will return immediately, and the roster will be 283 * reloaded at a later point when the server responds to the reload request. 284 * @throws NotLoggedInException If not logged in. 285 * @throws NotConnectedException 286 */ 287 public void reload() throws NotLoggedInException, NotConnectedException{ 288 final XMPPConnection connection = connection(); 289 if (!connection.isAuthenticated()) { 290 throw new NotLoggedInException(); 291 } 292 if (connection.isAnonymous()) { 293 throw new IllegalStateException("Anonymous users can't have a roster."); 294 } 295 296 RosterPacket packet = new RosterPacket(); 297 if (rosterStore != null && isRosterVersioningSupported()) { 298 packet.setVersion(rosterStore.getRosterVersion()); 299 } 300 rosterState = RosterState.loading; 301 connection.sendIqWithResponseCallback(packet, new RosterResultListener(), new ExceptionCallback() { 302 @Override 303 public void processException(Exception exception) { 304 rosterState = RosterState.uninitialized; 305 LOGGER.log(Level.SEVERE, "Exception reloading roster" , exception); 306 } 307 }); 308 } 309 310 /** 311 * Reload the roster and block until it is reloaded. 312 * 313 * @throws NotLoggedInException 314 * @throws NotConnectedException 315 * @throws InterruptedException 316 * @since 4.1 317 */ 318 public void reloadAndWait() throws NotLoggedInException, NotConnectedException, InterruptedException { 319 reload(); 320 waitUntilLoaded(); 321 } 322 323 /** 324 * Set the roster store, may cause a roster reload 325 * 326 * @param rosterStore 327 * @return true if the roster reload was initiated, false otherwise. 328 * @since 4.1 329 */ 330 public boolean setRosterStore(RosterStore rosterStore) { 331 this.rosterStore = rosterStore; 332 try { 333 reload(); 334 } 335 catch (NotLoggedInException | NotConnectedException e) { 336 LOGGER.log(Level.FINER, "Could not reload roster", e); 337 return false; 338 } 339 return true; 340 } 341 342 protected boolean waitUntilLoaded() throws InterruptedException { 343 long waitTime = connection().getPacketReplyTimeout(); 344 long start = System.currentTimeMillis(); 345 while (!isLoaded()) { 346 if (waitTime <= 0) { 347 break; 348 } 349 synchronized (this) { 350 if (!isLoaded()) { 351 wait(waitTime); 352 } 353 } 354 long now = System.currentTimeMillis(); 355 waitTime -= now - start; 356 start = now; 357 } 358 return isLoaded(); 359 } 360 361 /** 362 * Check if the roster is loaded. 363 * 364 * @return true if the roster is loaded. 365 * @since 4.1 366 */ 367 public boolean isLoaded() { 368 return rosterState == RosterState.loaded; 369 } 370 371 /** 372 * Adds a listener to this roster. The listener will be fired anytime one or more 373 * changes to the roster are pushed from the server. 374 * 375 * @param rosterListener a roster listener. 376 * @return true if the listener was not already added. 377 * @see #getEntriesAndAddListener(RosterListener, RosterEntries) 378 */ 379 public boolean addRosterListener(RosterListener rosterListener) { 380 synchronized (rosterListenersAndEntriesLock) { 381 return rosterListeners.add(rosterListener); 382 } 383 } 384 385 /** 386 * Removes a listener from this roster. The listener will be fired anytime one or more 387 * changes to the roster are pushed from the server. 388 * 389 * @param rosterListener a roster listener. 390 * @return true if the listener was active and got removed. 391 */ 392 public boolean removeRosterListener(RosterListener rosterListener) { 393 synchronized (rosterListenersAndEntriesLock) { 394 return rosterListeners.remove(rosterListener); 395 } 396 } 397 398 /** 399 * Add a roster loaded listener. 400 * 401 * @param rosterLoadedListener the listener to add. 402 * @return true if the listener was not already added. 403 * @see RosterLoadedListener 404 * @since 4.1 405 */ 406 public boolean addRosterLoadedListener(RosterLoadedListener rosterLoadedListener) { 407 synchronized (rosterLoadedListener) { 408 return rosterLoadedListeners.add(rosterLoadedListener); 409 } 410 } 411 412 /** 413 * Remove a roster loaded listener. 414 * 415 * @param rosterLoadedListener the listener to remove. 416 * @return true if the listener was active and got removed. 417 * @see RosterLoadedListener 418 * @since 4.1 419 */ 420 public boolean removeRosterLoadedListener(RosterLoadedListener rosterLoadedListener) { 421 synchronized (rosterLoadedListener) { 422 return rosterLoadedListeners.remove(rosterLoadedListener); 423 } 424 } 425 426 /** 427 * Creates a new group.<p> 428 * <p/> 429 * Note: you must add at least one entry to the group for the group to be kept 430 * after a logout/login. This is due to the way that XMPP stores group information. 431 * 432 * @param name the name of the group. 433 * @return a new group, or null if the group already exists 434 * @throws IllegalStateException if logged in anonymously 435 */ 436 public RosterGroup createGroup(String name) { 437 final XMPPConnection connection = connection(); 438 if (connection.isAnonymous()) { 439 throw new IllegalStateException("Anonymous users can't have a roster."); 440 } 441 if (groups.containsKey(name)) { 442 return groups.get(name); 443 } 444 445 RosterGroup group = new RosterGroup(name, connection); 446 groups.put(name, group); 447 return group; 448 } 449 450 /** 451 * Creates a new roster entry and presence subscription. The server will asynchronously 452 * update the roster with the subscription status. 453 * 454 * @param user the user. (e.g. johndoe@jabber.org) 455 * @param name the nickname of the user. 456 * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the 457 * the roster entry won't belong to a group. 458 * @throws NoResponseException if there was no response from the server. 459 * @throws XMPPErrorException if an XMPP exception occurs. 460 * @throws NotLoggedInException If not logged in. 461 * @throws NotConnectedException 462 */ 463 public void createEntry(String user, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException { 464 final XMPPConnection connection = connection(); 465 if (!connection.isAuthenticated()) { 466 throw new NotLoggedInException(); 467 } 468 if (connection.isAnonymous()) { 469 throw new IllegalStateException("Anonymous users can't have a roster."); 470 } 471 472 // Create and send roster entry creation packet. 473 RosterPacket rosterPacket = new RosterPacket(); 474 rosterPacket.setType(IQ.Type.set); 475 RosterPacket.Item item = new RosterPacket.Item(user, name); 476 if (groups != null) { 477 for (String group : groups) { 478 if (group != null && group.trim().length() > 0) { 479 item.addGroupName(group); 480 } 481 } 482 } 483 rosterPacket.addRosterItem(item); 484 connection.createPacketCollectorAndSend(rosterPacket).nextResultOrThrow(); 485 486 // Create a presence subscription packet and send. 487 Presence presencePacket = new Presence(Presence.Type.subscribe); 488 presencePacket.setTo(user); 489 connection.sendStanza(presencePacket); 490 } 491 492 /** 493 * Removes a roster entry from the roster. The roster entry will also be removed from the 494 * unfiled entries or from any roster group where it could belong and will no longer be part 495 * of the roster. Note that this is a synchronous call -- Smack must wait for the server 496 * to send an updated subscription status. 497 * 498 * @param entry a roster entry. 499 * @throws XMPPErrorException if an XMPP error occurs. 500 * @throws NotLoggedInException if not logged in. 501 * @throws NoResponseException SmackException if there was no response from the server. 502 * @throws NotConnectedException 503 * @throws IllegalStateException if connection is not logged in or logged in anonymously 504 */ 505 public void removeEntry(RosterEntry entry) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException { 506 final XMPPConnection connection = connection(); 507 if (!connection.isAuthenticated()) { 508 throw new NotLoggedInException(); 509 } 510 if (connection.isAnonymous()) { 511 throw new IllegalStateException("Anonymous users can't have a roster."); 512 } 513 514 // Only remove the entry if it's in the entry list. 515 // The actual removal logic takes place in RosterPacketListenerprocess>>Packet(Packet) 516 if (!entries.containsKey(entry.getUser())) { 517 return; 518 } 519 RosterPacket packet = new RosterPacket(); 520 packet.setType(IQ.Type.set); 521 RosterPacket.Item item = RosterEntry.toRosterItem(entry); 522 // Set the item type as REMOVE so that the server will delete the entry 523 item.setItemType(RosterPacket.ItemType.remove); 524 packet.addRosterItem(item); 525 connection.createPacketCollectorAndSend(packet).nextResultOrThrow(); 526 } 527 528 /** 529 * Returns a count of the entries in the roster. 530 * 531 * @return the number of entries in the roster. 532 */ 533 public int getEntryCount() { 534 return getEntries().size(); 535 } 536 537 /** 538 * Add a roster listener and invoke the roster entries with all entries of the roster. 539 * <p> 540 * The method guarantees that the listener is only invoked after 541 * {@link RosterEntries#rosterEntires(Collection)} has been invoked, and that all roster events 542 * that happen while <code>rosterEntires(Collection) </code> is called are queued until the 543 * method returns. 544 * </p> 545 * <p> 546 * This guarantee makes this the ideal method to e.g. populate a UI element with the roster while 547 * installing a {@link RosterListener} to listen for subsequent roster events. 548 * </p> 549 * 550 * @param rosterListener the listener to install 551 * @param rosterEntries the roster entries callback interface 552 * @since 4.1 553 */ 554 public void getEntriesAndAddListener(RosterListener rosterListener, RosterEntries rosterEntries) { 555 Objects.requireNonNull(rosterListener, "listener must not be null"); 556 Objects.requireNonNull(rosterEntries, "rosterEntries must not be null"); 557 558 synchronized (rosterListenersAndEntriesLock) { 559 rosterEntries.rosterEntires(entries.values()); 560 addRosterListener(rosterListener); 561 } 562 } 563 564 /** 565 * Returns a set of all entries in the roster, including entries 566 * that don't belong to any groups. 567 * 568 * @return all entries in the roster. 569 */ 570 public Set<RosterEntry> getEntries() { 571 Set<RosterEntry> allEntries; 572 synchronized (rosterListenersAndEntriesLock) { 573 allEntries = new HashSet<>(entries.size()); 574 for (RosterEntry entry : entries.values()) { 575 allEntries.add(entry); 576 } 577 } 578 return allEntries; 579 } 580 581 /** 582 * Returns a count of the unfiled entries in the roster. An unfiled entry is 583 * an entry that doesn't belong to any groups. 584 * 585 * @return the number of unfiled entries in the roster. 586 */ 587 public int getUnfiledEntryCount() { 588 return unfiledEntries.size(); 589 } 590 591 /** 592 * Returns an unmodifiable set for the unfiled roster entries. An unfiled entry is 593 * an entry that doesn't belong to any groups. 594 * 595 * @return the unfiled roster entries. 596 */ 597 public Set<RosterEntry> getUnfiledEntries() { 598 return Collections.unmodifiableSet(unfiledEntries); 599 } 600 601 /** 602 * Returns the roster entry associated with the given XMPP address or 603 * <tt>null</tt> if the user is not an entry in the roster. 604 * 605 * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be 606 * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource"). 607 * @return the roster entry or <tt>null</tt> if it does not exist. 608 */ 609 public RosterEntry getEntry(String user) { 610 if (user == null) { 611 return null; 612 } 613 String key = getMapKey(user); 614 return entries.get(key); 615 } 616 617 /** 618 * Returns true if the specified XMPP address is an entry in the roster. 619 * 620 * @param user the XMPP address of the user (eg "jsmith@example.com"). The 621 * address could be in any valid format (e.g. "domain/resource", 622 * "user@domain" or "user@domain/resource"). 623 * @return true if the XMPP address is an entry in the roster. 624 */ 625 public boolean contains(String user) { 626 return getEntry(user) != null; 627 } 628 629 /** 630 * Returns the roster group with the specified name, or <tt>null</tt> if the 631 * group doesn't exist. 632 * 633 * @param name the name of the group. 634 * @return the roster group with the specified name. 635 */ 636 public RosterGroup getGroup(String name) { 637 return groups.get(name); 638 } 639 640 /** 641 * Returns the number of the groups in the roster. 642 * 643 * @return the number of groups in the roster. 644 */ 645 public int getGroupCount() { 646 return groups.size(); 647 } 648 649 /** 650 * Returns an unmodifiable collections of all the roster groups. 651 * 652 * @return an iterator for all roster groups. 653 */ 654 public Collection<RosterGroup> getGroups() { 655 return Collections.unmodifiableCollection(groups.values()); 656 } 657 658 /** 659 * Returns the presence info for a particular user. If the user is offline, or 660 * if no presence data is available (such as when you are not subscribed to the 661 * user's presence updates), unavailable presence will be returned. 662 * <p> 663 * If the user has several presences (one for each resource), then the presence with 664 * highest priority will be returned. If multiple presences have the same priority, 665 * the one with the "most available" presence mode will be returned. In order, 666 * that's {@link org.jivesoftware.smack.packet.Presence.Mode#chat free to chat}, 667 * {@link org.jivesoftware.smack.packet.Presence.Mode#available available}, 668 * {@link org.jivesoftware.smack.packet.Presence.Mode#away away}, 669 * {@link org.jivesoftware.smack.packet.Presence.Mode#xa extended away}, and 670 * {@link org.jivesoftware.smack.packet.Presence.Mode#dnd do not disturb}.<p> 671 * </p> 672 * <p> 673 * Note that presence information is received asynchronously. So, just after logging 674 * in to the server, presence values for users in the roster may be unavailable 675 * even if they are actually online. In other words, the value returned by this 676 * method should only be treated as a snapshot in time, and may not accurately reflect 677 * other user's presence instant by instant. If you need to track presence over time, 678 * such as when showing a visual representation of the roster, consider using a 679 * {@link RosterListener}. 680 * </p> 681 * 682 * @param user an XMPP ID. The address could be in any valid format (e.g. 683 * "domain/resource", "user@domain" or "user@domain/resource"). Any resource 684 * information that's part of the ID will be discarded. 685 * @return the user's current presence, or unavailable presence if the user is offline 686 * or if no presence information is available.. 687 */ 688 public Presence getPresence(String user) { 689 String key = getMapKey(XmppStringUtils.parseBareJid(user)); 690 Map<String, Presence> userPresences = presenceMap.get(key); 691 if (userPresences == null) { 692 Presence presence = new Presence(Presence.Type.unavailable); 693 presence.setFrom(user); 694 return presence; 695 } 696 else { 697 // Find the resource with the highest priority 698 // Might be changed to use the resource with the highest availability instead. 699 Presence presence = null; 700 // This is used in case no available presence is found 701 Presence unavailable = null; 702 703 for (String resource : userPresences.keySet()) { 704 Presence p = userPresences.get(resource); 705 if (!p.isAvailable()) { 706 unavailable = p; 707 continue; 708 } 709 // Chose presence with highest priority first. 710 if (presence == null || p.getPriority() > presence.getPriority()) { 711 presence = p; 712 } 713 // If equal priority, choose "most available" by the mode value. 714 else if (p.getPriority() == presence.getPriority()) { 715 Presence.Mode pMode = p.getMode(); 716 // Default to presence mode of available. 717 if (pMode == null) { 718 pMode = Presence.Mode.available; 719 } 720 Presence.Mode presenceMode = presence.getMode(); 721 // Default to presence mode of available. 722 if (presenceMode == null) { 723 presenceMode = Presence.Mode.available; 724 } 725 if (pMode.compareTo(presenceMode) < 0) { 726 presence = p; 727 } 728 } 729 } 730 if (presence == null) { 731 if (unavailable != null) { 732 return unavailable.clone(); 733 } 734 else { 735 presence = new Presence(Presence.Type.unavailable); 736 presence.setFrom(user); 737 return presence; 738 } 739 } 740 else { 741 return presence.clone(); 742 } 743 } 744 } 745 746 /** 747 * Returns the presence info for a particular user's resource, or unavailable presence 748 * if the user is offline or if no presence information is available, such as 749 * when you are not subscribed to the user's presence updates. 750 * 751 * @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource). 752 * @return the user's current presence, or unavailable presence if the user is offline 753 * or if no presence information is available. 754 */ 755 public Presence getPresenceResource(String userWithResource) { 756 String key = getMapKey(userWithResource); 757 String resource = XmppStringUtils.parseResource(userWithResource); 758 Map<String, Presence> userPresences = presenceMap.get(key); 759 if (userPresences == null) { 760 Presence presence = new Presence(Presence.Type.unavailable); 761 presence.setFrom(userWithResource); 762 return presence; 763 } 764 else { 765 Presence presence = userPresences.get(resource); 766 if (presence == null) { 767 presence = new Presence(Presence.Type.unavailable); 768 presence.setFrom(userWithResource); 769 return presence; 770 } 771 else { 772 return presence.clone(); 773 } 774 } 775 } 776 777 /** 778 * Returns a List of Presence objects for all of a user's current presences if no presence information is available, 779 * such as when you are not subscribed to the user's presence updates. 780 * 781 * @param bareJid an XMPP ID, e.g. jdoe@example.com. 782 * @return a List of Presence objects for all the user's current presences, or an unavailable presence if no 783 * presence information is available. 784 */ 785 public List<Presence> getAllPresences(String bareJid) { 786 Map<String, Presence> userPresences = presenceMap.get(getMapKey(bareJid)); 787 List<Presence> res; 788 if (userPresences == null) { 789 // Create an unavailable presence if none was found 790 Presence unavailable = new Presence(Presence.Type.unavailable); 791 unavailable.setFrom(bareJid); 792 res = new ArrayList<>(Arrays.asList(unavailable)); 793 } else { 794 res = new ArrayList<>(userPresences.values().size()); 795 for (Presence presence : userPresences.values()) { 796 res.add(presence.clone()); 797 } 798 } 799 return res; 800 } 801 802 /** 803 * Returns a List of all <b>available</b> Presence Objects for the given bare JID. If there are no available 804 * presences, then the empty list will be returned. 805 * 806 * @param bareJid the bare JID from which the presences should be retrieved. 807 * @return available presences for the bare JID. 808 */ 809 public List<Presence> getAvailablePresences(String bareJid) { 810 List<Presence> allPresences = getAllPresences(bareJid); 811 List<Presence> res = new ArrayList<>(allPresences.size()); 812 for (Presence presence : allPresences) { 813 if (presence.isAvailable()) { 814 // No need to clone presence here, getAllPresences already returns clones 815 res.add(presence); 816 } 817 } 818 return res; 819 } 820 821 /** 822 * Returns a List of Presence objects for all of a user's current presences 823 * or an unavailable presence if the user is unavailable (offline) or if no presence 824 * information is available, such as when you are not subscribed to the user's presence 825 * updates. 826 * 827 * @param user an XMPP ID, e.g. jdoe@example.com. 828 * @return a List of Presence objects for all the user's current presences, 829 * or an unavailable presence if the user is offline or if no presence information 830 * is available. 831 */ 832 public List<Presence> getPresences(String user) { 833 List<Presence> res; 834 String key = getMapKey(user); 835 Map<String, Presence> userPresences = presenceMap.get(key); 836 if (userPresences == null) { 837 Presence presence = new Presence(Presence.Type.unavailable); 838 presence.setFrom(user); 839 res = Arrays.asList(presence); 840 } 841 else { 842 List<Presence> answer = new ArrayList<Presence>(); 843 // Used in case no available presence is found 844 Presence unavailable = null; 845 for (Presence presence : userPresences.values()) { 846 if (presence.isAvailable()) { 847 answer.add(presence.clone()); 848 } 849 else { 850 unavailable = presence; 851 } 852 } 853 if (!answer.isEmpty()) { 854 res = answer; 855 } 856 else if (unavailable != null) { 857 res = Arrays.asList(unavailable.clone()); 858 } 859 else { 860 Presence presence = new Presence(Presence.Type.unavailable); 861 presence.setFrom(user); 862 res = Arrays.asList(presence); 863 } 864 } 865 return res; 866 } 867 868 /** 869 * Check if the given JID is subscribed to the user's presence. 870 * <p> 871 * If the JID is subscribed to the user's presence then it is allowed to see the presence and 872 * will get notified about presence changes. Also returns true, if the JID is the service 873 * name of the XMPP connection (the "XMPP domain"), i.e. the XMPP service is treated like 874 * having an implicit subscription to the users presence. 875 * </p> 876 * Note that if the roster is not loaded, then this method will always return false. 877 * 878 * @param jid 879 * @return true if the given JID is allowed to see the users presence. 880 * @since 4.1 881 */ 882 public boolean isSubscribedToMyPresence(String jid) { 883 if (connection().getServiceName().equals(jid)) { 884 return true; 885 } 886 RosterEntry entry = getEntry(jid); 887 if (entry == null) { 888 return false; 889 } 890 switch (entry.getType()) { 891 case from: 892 case both: 893 return true; 894 default: 895 return false; 896 } 897 } 898 899 /** 900 * Sets if the roster will be loaded from the server when logging in for newly created instances 901 * of {@link Roster}. 902 * 903 * @param rosterLoadedAtLoginDefault if the roster will be loaded from the server when logging in. 904 * @see #setRosterLoadedAtLogin(boolean) 905 * @since 4.1.7 906 */ 907 public static void setRosterLoadedAtLoginDefault(boolean rosterLoadedAtLoginDefault) { 908 Roster.rosterLoadedAtLoginDefault = rosterLoadedAtLoginDefault; 909 } 910 911 /** 912 * Sets if the roster will be loaded from the server when logging in. This 913 * is the common behaviour for clients but sometimes clients may want to differ this 914 * or just never do it if not interested in rosters. 915 * 916 * @param rosterLoadedAtLogin if the roster will be loaded from the server when logging in. 917 */ 918 public void setRosterLoadedAtLogin(boolean rosterLoadedAtLogin) { 919 this.rosterLoadedAtLogin = rosterLoadedAtLogin; 920 } 921 922 /** 923 * Returns true if the roster will be loaded from the server when logging in. This 924 * is the common behavior for clients but sometimes clients may want to differ this 925 * or just never do it if not interested in rosters. 926 * 927 * @return true if the roster will be loaded from the server when logging in. 928 * @see <a href="http://xmpp.org/rfcs/rfc6121.html#roster-login">RFC 6121 2.2 - Retrieving the Roster on Login</a> 929 */ 930 public boolean isRosterLoadedAtLogin() { 931 return rosterLoadedAtLogin; 932 } 933 934 RosterStore getRosterStore() { 935 return rosterStore; 936 } 937 938 /** 939 * Returns the key to use in the presenceMap and entries Map for a fully qualified XMPP ID. 940 * The roster can contain any valid address format such us "domain/resource", 941 * "user@domain" or "user@domain/resource". If the roster contains an entry 942 * associated with the fully qualified XMPP ID then use the fully qualified XMPP 943 * ID as the key in presenceMap, otherwise use the bare address. Note: When the 944 * key in presenceMap is a fully qualified XMPP ID, the userPresences is useless 945 * since it will always contain one entry for the user. 946 * 947 * @param user the bare or fully qualified XMPP ID, e.g. jdoe@example.com or 948 * jdoe@example.com/Work. 949 * @return the key to use in the presenceMap and entries Map for the fully qualified XMPP ID. 950 */ 951 private String getMapKey(String user) { 952 if (user == null) { 953 return null; 954 } 955 if (entries.containsKey(user)) { 956 return user; 957 } 958 String key = XmppStringUtils.parseBareJid(user); 959 return key.toLowerCase(Locale.US); 960 } 961 962 /** 963 * Changes the presence of available contacts offline by simulating an unavailable 964 * presence sent from the server. After a disconnection, every Presence is set 965 * to offline. 966 * @throws NotConnectedException 967 */ 968 private void setOfflinePresencesAndResetLoaded() { 969 Presence packetUnavailable; 970 for (String user : presenceMap.keySet()) { 971 Map<String, Presence> resources = presenceMap.get(user); 972 if (resources != null) { 973 for (String resource : resources.keySet()) { 974 packetUnavailable = new Presence(Presence.Type.unavailable); 975 packetUnavailable.setFrom(user + "/" + resource); 976 try { 977 presencePacketListener.processPacket(packetUnavailable); 978 } 979 catch (NotConnectedException e) { 980 throw new IllegalStateException( 981 "presencePakcetListener should never throw a NotConnectedException when processPacket is called with a presence of type unavailable", 982 e); 983 } 984 } 985 } 986 } 987 rosterState = RosterState.uninitialized; 988 } 989 990 /** 991 * Fires roster changed event to roster listeners indicating that the 992 * specified collections of contacts have been added, updated or deleted 993 * from the roster. 994 * 995 * @param addedEntries the collection of address of the added contacts. 996 * @param updatedEntries the collection of address of the updated contacts. 997 * @param deletedEntries the collection of address of the deleted contacts. 998 */ 999 private void fireRosterChangedEvent(final Collection<String> addedEntries, final Collection<String> updatedEntries, 1000 final Collection<String> deletedEntries) { 1001 synchronized (rosterListenersAndEntriesLock) { 1002 for (RosterListener listener : rosterListeners) { 1003 if (!addedEntries.isEmpty()) { 1004 listener.entriesAdded(addedEntries); 1005 } 1006 if (!updatedEntries.isEmpty()) { 1007 listener.entriesUpdated(updatedEntries); 1008 } 1009 if (!deletedEntries.isEmpty()) { 1010 listener.entriesDeleted(deletedEntries); 1011 } 1012 } 1013 } 1014 } 1015 1016 /** 1017 * Fires roster presence changed event to roster listeners. 1018 * 1019 * @param presence the presence change. 1020 */ 1021 private void fireRosterPresenceEvent(final Presence presence) { 1022 synchronized (rosterListenersAndEntriesLock) { 1023 for (RosterListener listener : rosterListeners) { 1024 listener.presenceChanged(presence); 1025 } 1026 } 1027 } 1028 1029 private void addUpdateEntry(Collection<String> addedEntries, Collection<String> updatedEntries, 1030 Collection<String> unchangedEntries, RosterPacket.Item item, RosterEntry entry) { 1031 RosterEntry oldEntry; 1032 synchronized (rosterListenersAndEntriesLock) { 1033 oldEntry = entries.put(item.getUser(), entry); 1034 } 1035 if (oldEntry == null) { 1036 addedEntries.add(item.getUser()); 1037 } 1038 else { 1039 RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry); 1040 if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) { 1041 updatedEntries.add(item.getUser()); 1042 } else { 1043 // Record the entry as unchanged, so that it doesn't end up as deleted entry 1044 unchangedEntries.add(item.getUser()); 1045 } 1046 } 1047 1048 // Mark the entry as unfiled if it does not belong to any groups. 1049 if (item.getGroupNames().isEmpty()) { 1050 unfiledEntries.add(entry); 1051 } 1052 else { 1053 unfiledEntries.remove(entry); 1054 } 1055 1056 // Add the entry/user to the groups 1057 List<String> newGroupNames = new ArrayList<String>(); 1058 for (String groupName : item.getGroupNames()) { 1059 // Add the group name to the list. 1060 newGroupNames.add(groupName); 1061 1062 // Add the entry to the group. 1063 RosterGroup group = getGroup(groupName); 1064 if (group == null) { 1065 group = createGroup(groupName); 1066 groups.put(groupName, group); 1067 } 1068 // Add the entry. 1069 group.addEntryLocal(entry); 1070 } 1071 1072 // Remove user from the remaining groups. 1073 List<String> oldGroupNames = new ArrayList<String>(); 1074 for (RosterGroup group: getGroups()) { 1075 oldGroupNames.add(group.getName()); 1076 } 1077 oldGroupNames.removeAll(newGroupNames); 1078 1079 for (String groupName : oldGroupNames) { 1080 RosterGroup group = getGroup(groupName); 1081 group.removeEntryLocal(entry); 1082 if (group.getEntryCount() == 0) { 1083 groups.remove(groupName); 1084 } 1085 } 1086 } 1087 1088 private void deleteEntry(Collection<String> deletedEntries, RosterEntry entry) { 1089 String user = entry.getUser(); 1090 entries.remove(user); 1091 unfiledEntries.remove(entry); 1092 presenceMap.remove(XmppStringUtils.parseBareJid(user)); 1093 deletedEntries.add(user); 1094 1095 for (Entry<String,RosterGroup> e: groups.entrySet()) { 1096 RosterGroup group = e.getValue(); 1097 group.removeEntryLocal(entry); 1098 if (group.getEntryCount() == 0) { 1099 groups.remove(e.getKey()); 1100 } 1101 } 1102 } 1103 1104 1105 /** 1106 * Removes all the groups with no entries. 1107 * 1108 * This is used by {@link RosterPushListener} and {@link RosterResultListener} to 1109 * cleanup groups after removing contacts. 1110 */ 1111 private void removeEmptyGroups() { 1112 // We have to do this because RosterGroup.removeEntry removes the entry immediately 1113 // (locally) and the group could remain empty. 1114 // TODO Check the performance/logic for rosters with large number of groups 1115 for (RosterGroup group : getGroups()) { 1116 if (group.getEntryCount() == 0) { 1117 groups.remove(group.getName()); 1118 } 1119 } 1120 } 1121 1122 /** 1123 * Ignore ItemTypes as of RFC 6121, 2.1.2.5. 1124 * 1125 * This is used by {@link RosterPushListener} and {@link RosterResultListener}. 1126 * */ 1127 private static boolean hasValidSubscriptionType(RosterPacket.Item item) { 1128 switch (item.getItemType()) { 1129 case none: 1130 case from: 1131 case to: 1132 case both: 1133 return true; 1134 default: 1135 return false; 1136 } 1137 } 1138 1139 /** 1140 * Check if the server supports roster versioning. 1141 * 1142 * @return true if the server supports roster versioning, false otherwise. 1143 */ 1144 public boolean isRosterVersioningSupported() { 1145 return connection().hasFeature(RosterVer.ELEMENT, RosterVer.NAMESPACE); 1146 } 1147 1148 /** 1149 * An enumeration for the subscription mode options. 1150 */ 1151 public enum SubscriptionMode { 1152 1153 /** 1154 * Automatically accept all subscription and unsubscription requests. This is 1155 * the default mode and is suitable for simple client. More complex client will 1156 * likely wish to handle subscription requests manually. 1157 */ 1158 accept_all, 1159 1160 /** 1161 * Automatically reject all subscription requests. 1162 */ 1163 reject_all, 1164 1165 /** 1166 * Subscription requests are ignored, which means they must be manually 1167 * processed by registering a listener for presence packets and then looking 1168 * for any presence requests that have the type Presence.Type.SUBSCRIBE or 1169 * Presence.Type.UNSUBSCRIBE. 1170 */ 1171 manual 1172 } 1173 1174 /** 1175 * Listens for all presence packets and processes them. 1176 */ 1177 private class PresencePacketListener implements StanzaListener { 1178 1179 /** 1180 * Retrieve the user presences (a map from resource to {@link Presence}) for a given key (usually a JID without 1181 * a resource). If the {@link #presenceMap} does not contain already a user presence map, then it will be 1182 * created. 1183 * 1184 * @param key the presence map key 1185 * @return the user presences 1186 */ 1187 private Map<String, Presence> getUserPresences(String key) { 1188 Map<String, Presence> userPresences = presenceMap.get(key); 1189 if (userPresences == null) { 1190 userPresences = new ConcurrentHashMap<>(); 1191 presenceMap.put(key, userPresences); 1192 } 1193 return userPresences; 1194 } 1195 1196 @Override 1197 public void processPacket(Stanza packet) throws NotConnectedException { 1198 // Try to ensure that the roster is loaded when processing presence stanzas. While the 1199 // presence listener is synchronous, the roster result listener is not, which means that 1200 // the presence listener may be invoked with a not yet loaded roster. 1201 if (rosterState == RosterState.loading) { 1202 try { 1203 waitUntilLoaded(); 1204 } 1205 catch (InterruptedException e) { 1206 LOGGER.log(Level.INFO, "Presence listener was interrupted", e); 1207 1208 } 1209 } 1210 if (!isLoaded() && rosterLoadedAtLogin) { 1211 LOGGER.warning("Roster not loaded while processing presence stanza"); 1212 } 1213 final XMPPConnection connection = connection(); 1214 Presence presence = (Presence) packet; 1215 String from = presence.getFrom(); 1216 String key = getMapKey(from); 1217 Map<String, Presence> userPresences; 1218 Presence response = null; 1219 1220 // If an "available" presence, add it to the presence map. Each presence 1221 // map will hold for a particular user a map with the presence 1222 // packets saved for each resource. 1223 switch (presence.getType()) { 1224 case available: 1225 // Get the user presence map 1226 userPresences = getUserPresences(key); 1227 // See if an offline presence was being stored in the map. If so, remove 1228 // it since we now have an online presence. 1229 userPresences.remove(""); 1230 // Add the new presence, using the resources as a key. 1231 userPresences.put(XmppStringUtils.parseResource(from), presence); 1232 // If the user is in the roster, fire an event. 1233 if (entries.containsKey(key)) { 1234 fireRosterPresenceEvent(presence); 1235 } 1236 break; 1237 // If an "unavailable" packet. 1238 case unavailable: 1239 // If no resource, this is likely an offline presence as part of 1240 // a roster presence flood. In that case, we store it. 1241 if ("".equals(XmppStringUtils.parseResource(from))) { 1242 // Get the user presence map 1243 userPresences = getUserPresences(key); 1244 userPresences.put("", presence); 1245 } 1246 // Otherwise, this is a normal offline presence. 1247 else if (presenceMap.get(key) != null) { 1248 userPresences = presenceMap.get(key); 1249 // Store the offline presence, as it may include extra information 1250 // such as the user being on vacation. 1251 userPresences.put(XmppStringUtils.parseResource(from), presence); 1252 } 1253 // If the user is in the roster, fire an event. 1254 if (entries.containsKey(key)) { 1255 fireRosterPresenceEvent(presence); 1256 } 1257 break; 1258 case subscribe: 1259 switch (subscriptionMode) { 1260 case accept_all: 1261 // Accept all subscription requests. 1262 response = new Presence(Presence.Type.subscribed); 1263 break; 1264 case reject_all: 1265 // Reject all subscription requests. 1266 response = new Presence(Presence.Type.unsubscribed); 1267 break; 1268 case manual: 1269 default: 1270 // Otherwise, in manual mode so ignore. 1271 break; 1272 } 1273 if (response != null) { 1274 response.setTo(presence.getFrom()); 1275 connection.sendStanza(response); 1276 } 1277 break; 1278 case unsubscribe: 1279 if (subscriptionMode != SubscriptionMode.manual) { 1280 // Acknowledge and accept unsubscription notification so that the 1281 // server will stop sending notifications saying that the contact 1282 // has unsubscribed to our presence. 1283 response = new Presence(Presence.Type.unsubscribed); 1284 response.setTo(presence.getFrom()); 1285 connection.sendStanza(response); 1286 } 1287 // Otherwise, in manual mode so ignore. 1288 break; 1289 // Error presence packets from a bare JID mean we invalidate all existing 1290 // presence info for the user. 1291 case error: 1292 if (!"".equals(XmppStringUtils.parseResource(from))) { 1293 break; 1294 } 1295 userPresences = getUserPresences(key); 1296 // Any other presence data is invalidated by the error packet. 1297 userPresences.clear(); 1298 1299 // Set the new presence using the empty resource as a key. 1300 userPresences.put("", presence); 1301 // If the user is in the roster, fire an event. 1302 if (entries.containsKey(key)) { 1303 fireRosterPresenceEvent(presence); 1304 } 1305 break; 1306 default: 1307 break; 1308 } 1309 } 1310 } 1311 1312 /** 1313 * Handles roster reults as described in RFC 6121 2.1.4 1314 */ 1315 private class RosterResultListener implements StanzaListener { 1316 1317 @Override 1318 public void processPacket(Stanza packet) { 1319 final XMPPConnection connection = connection(); 1320 LOGGER.fine("RosterResultListener received stanza"); 1321 Collection<String> addedEntries = new ArrayList<String>(); 1322 Collection<String> updatedEntries = new ArrayList<String>(); 1323 Collection<String> deletedEntries = new ArrayList<String>(); 1324 Collection<String> unchangedEntries = new ArrayList<String>(); 1325 1326 if (packet instanceof RosterPacket) { 1327 // Non-empty roster result. This stanza contains all the roster elements. 1328 RosterPacket rosterPacket = (RosterPacket) packet; 1329 1330 // Ignore items without valid subscription type 1331 ArrayList<Item> validItems = new ArrayList<RosterPacket.Item>(); 1332 for (RosterPacket.Item item : rosterPacket.getRosterItems()) { 1333 if (hasValidSubscriptionType(item)) { 1334 validItems.add(item); 1335 } 1336 } 1337 1338 for (RosterPacket.Item item : validItems) { 1339 RosterEntry entry = new RosterEntry(item.getUser(), item.getName(), 1340 item.getItemType(), item.getItemStatus(), Roster.this, connection); 1341 addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry); 1342 } 1343 1344 // Delete all entries which where not added or updated 1345 Set<String> toDelete = new HashSet<String>(); 1346 for (RosterEntry entry : entries.values()) { 1347 toDelete.add(entry.getUser()); 1348 } 1349 toDelete.removeAll(addedEntries); 1350 toDelete.removeAll(updatedEntries); 1351 toDelete.removeAll(unchangedEntries); 1352 for (String user : toDelete) { 1353 deleteEntry(deletedEntries, entries.get(user)); 1354 } 1355 1356 if (rosterStore != null) { 1357 String version = rosterPacket.getVersion(); 1358 rosterStore.resetEntries(validItems, version); 1359 } 1360 1361 removeEmptyGroups(); 1362 } 1363 else { 1364 // Empty roster result as defined in RFC6121 2.6.3. An empty roster result basically 1365 // means that rosterver was used and the roster hasn't changed (much) since the 1366 // version we presented the server. So we simply load the roster from the store and 1367 // await possible further roster pushes. 1368 for (RosterPacket.Item item : rosterStore.getEntries()) { 1369 RosterEntry entry = new RosterEntry(item.getUser(), item.getName(), 1370 item.getItemType(), item.getItemStatus(), Roster.this, connection); 1371 addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry); 1372 } 1373 } 1374 1375 rosterState = RosterState.loaded; 1376 synchronized (Roster.this) { 1377 Roster.this.notifyAll(); 1378 } 1379 // Fire event for roster listeners. 1380 fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); 1381 1382 // Call the roster loaded listeners after the roster events have been fired. This is 1383 // imporant because the user may call getEntriesAndAddListener() in onRosterLoaded(), 1384 // and if the order would be the other way around, the roster listener added by 1385 // getEntriesAndAddListener() would be invoked with information that was already 1386 // available at the time getEntriesAndAddListenr() was called. 1387 try { 1388 synchronized (rosterLoadedListeners) { 1389 for (RosterLoadedListener rosterLoadedListener : rosterLoadedListeners) { 1390 rosterLoadedListener.onRosterLoaded(Roster.this); 1391 } 1392 } 1393 } 1394 catch (Exception e) { 1395 LOGGER.log(Level.WARNING, "RosterLoadedListener threw exception", e); 1396 } 1397 } 1398 } 1399 1400 /** 1401 * Listens for all roster pushes and processes them. 1402 */ 1403 private class RosterPushListener extends AbstractIqRequestHandler { 1404 1405 private RosterPushListener() { 1406 super(RosterPacket.ELEMENT, RosterPacket.NAMESPACE, Type.set, Mode.sync); 1407 } 1408 1409 @Override 1410 public IQ handleIQRequest(IQ iqRequest) { 1411 final XMPPConnection connection = connection(); 1412 RosterPacket rosterPacket = (RosterPacket) iqRequest; 1413 1414 // Roster push (RFC 6121, 2.1.6) 1415 // A roster push with a non-empty from not matching our address MUST be ignored 1416 String jid = XmppStringUtils.parseBareJid(connection.getUser()); 1417 String from = rosterPacket.getFrom(); 1418 if (from != null && !from.equals(jid)) { 1419 LOGGER.warning("Ignoring roster push with a non matching 'from' ourJid='" + jid + "' from='" + from 1420 + "'"); 1421 return IQ.createErrorResponse(iqRequest, new XMPPError(Condition.service_unavailable)); 1422 } 1423 1424 // A roster push must contain exactly one entry 1425 Collection<Item> items = rosterPacket.getRosterItems(); 1426 if (items.size() != 1) { 1427 LOGGER.warning("Ignoring roster push with not exaclty one entry. size=" + items.size()); 1428 return IQ.createErrorResponse(iqRequest, new XMPPError(Condition.bad_request)); 1429 } 1430 1431 Collection<String> addedEntries = new ArrayList<String>(); 1432 Collection<String> updatedEntries = new ArrayList<String>(); 1433 Collection<String> deletedEntries = new ArrayList<String>(); 1434 Collection<String> unchangedEntries = new ArrayList<String>(); 1435 1436 // We assured above that the size of items is exaclty 1, therefore we are able to 1437 // safely retrieve this single item here. 1438 Item item = items.iterator().next(); 1439 RosterEntry entry = new RosterEntry(item.getUser(), item.getName(), 1440 item.getItemType(), item.getItemStatus(), Roster.this, connection); 1441 String version = rosterPacket.getVersion(); 1442 1443 if (item.getItemType().equals(RosterPacket.ItemType.remove)) { 1444 deleteEntry(deletedEntries, entry); 1445 if (rosterStore != null) { 1446 rosterStore.removeEntry(entry.getUser(), version); 1447 } 1448 } 1449 else if (hasValidSubscriptionType(item)) { 1450 addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry); 1451 if (rosterStore != null) { 1452 rosterStore.addEntry(item, version); 1453 } 1454 } 1455 1456 removeEmptyGroups(); 1457 1458 // Fire event for roster listeners. 1459 fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); 1460 1461 return IQ.createResultIQ(rosterPacket); 1462 } 1463 } 1464}