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