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; 019 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Locale; 027import java.util.Map; 028import java.util.Map.Entry; 029import java.util.Set; 030import java.util.concurrent.ConcurrentHashMap; 031import java.util.concurrent.CopyOnWriteArrayList; 032import java.util.logging.Level; 033import java.util.logging.Logger; 034 035import org.jivesoftware.smack.SmackException.NoResponseException; 036import org.jivesoftware.smack.SmackException.NotConnectedException; 037import org.jivesoftware.smack.SmackException.NotLoggedInException; 038import org.jivesoftware.smack.XMPPException.XMPPErrorException; 039import org.jivesoftware.smack.filter.AndFilter; 040import org.jivesoftware.smack.filter.IQReplyFilter; 041import org.jivesoftware.smack.filter.IQTypeFilter; 042import org.jivesoftware.smack.filter.PacketFilter; 043import org.jivesoftware.smack.filter.PacketTypeFilter; 044import org.jivesoftware.smack.packet.IQ; 045import org.jivesoftware.smack.packet.Packet; 046import org.jivesoftware.smack.packet.Presence; 047import org.jivesoftware.smack.packet.RosterPacket; 048import org.jivesoftware.smack.packet.RosterPacket.Item; 049import org.jivesoftware.smack.util.StringUtils; 050 051/** 052 * Represents a user's roster, which is the collection of users a person receives 053 * presence updates for. Roster items are categorized into groups for easier management.<p> 054 * <p/> 055 * Others users may attempt to subscribe to this user using a subscription request. Three 056 * modes are supported for handling these requests: <ul> 057 * <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li> 058 * <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li> 059 * <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li> 060 * </ul> 061 * 062 * @author Matt Tucker 063 * @see XMPPConnection#getRoster() 064 */ 065public class Roster { 066 067 private static final Logger LOGGER = Logger.getLogger(Roster.class.getName()); 068 069 private static final PacketFilter ROSTER_PUSH_FILTER = new AndFilter(new PacketTypeFilter( 070 RosterPacket.class), new IQTypeFilter(IQ.Type.SET)); 071 072 private static final PacketFilter PRESENCE_PACKET_FILTER = new PacketTypeFilter(Presence.class); 073 074 /** 075 * The default subscription processing mode to use when a Roster is created. By default 076 * all subscription requests are automatically accepted. 077 */ 078 private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.accept_all; 079 080 private final XMPPConnection connection; 081 private final RosterStore rosterStore; 082 private final Map<String, RosterGroup> groups = new ConcurrentHashMap<String, RosterGroup>(); 083 private final Map<String,RosterEntry> entries = new ConcurrentHashMap<String,RosterEntry>(); 084 private final List<RosterEntry> unfiledEntries = new CopyOnWriteArrayList<RosterEntry>(); 085 private final List<RosterListener> rosterListeners = new CopyOnWriteArrayList<RosterListener>(); 086 private final Map<String, Map<String, Presence>> presenceMap = new ConcurrentHashMap<String, Map<String, Presence>>(); 087 // The roster is marked as initialized when at least a single roster packet 088 // has been received and processed. 089 boolean rosterInitialized = false; 090 private final PresencePacketListener presencePacketListener = new PresencePacketListener(); 091 092 private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode(); 093 094 /** 095 * Returns the default subscription processing mode to use when a new Roster is created. The 096 * subscription processing mode dictates what action Smack will take when subscription 097 * requests from other users are made. The default subscription mode 098 * is {@link SubscriptionMode#accept_all}. 099 * 100 * @return the default subscription mode to use for new Rosters 101 */ 102 public static SubscriptionMode getDefaultSubscriptionMode() { 103 return defaultSubscriptionMode; 104 } 105 106 /** 107 * Sets the default subscription processing mode to use when a new Roster is created. The 108 * subscription processing mode dictates what action Smack will take when subscription 109 * requests from other users are made. The default subscription mode 110 * is {@link SubscriptionMode#accept_all}. 111 * 112 * @param subscriptionMode the default subscription mode to use for new Rosters. 113 */ 114 public static void setDefaultSubscriptionMode(SubscriptionMode subscriptionMode) { 115 defaultSubscriptionMode = subscriptionMode; 116 } 117 118 /** 119 * Creates a new roster. 120 * 121 * @param connection an XMPP connection. 122 */ 123 Roster(final XMPPConnection connection) { 124 this.connection = connection; 125 rosterStore = connection.getConfiguration().getRosterStore(); 126 // Listen for any roster packets. 127 connection.addPacketListener(new RosterPushListener(), ROSTER_PUSH_FILTER); 128 // Listen for any presence packets. 129 connection.addPacketListener(presencePacketListener, PRESENCE_PACKET_FILTER); 130 131 // Listen for connection events 132 connection.addConnectionListener(new AbstractConnectionListener() { 133 134 public void connectionClosed() { 135 // Changes the presence available contacts to unavailable 136 try { 137 setOfflinePresences(); 138 } 139 catch (NotConnectedException e) { 140 LOGGER.log(Level.SEVERE, "Not connected exception" ,e); 141 } 142 } 143 144 public void connectionClosedOnError(Exception e) { 145 // Changes the presence available contacts to unavailable 146 try { 147 setOfflinePresences(); 148 } 149 catch (NotConnectedException e1) { 150 LOGGER.log(Level.SEVERE, "Not connected exception" ,e); 151 } 152 } 153 154 }); 155 // If the connection is already established, call reload 156 if (connection.isAuthenticated()) { 157 try { 158 reload(); 159 } 160 catch (SmackException e) { 161 LOGGER.log(Level.SEVERE, "Could not reload Roster", e); 162 } 163 } 164 connection.addConnectionListener(new AbstractConnectionListener() { 165 public void authenticated(XMPPConnection connection) { 166 // Anonymous users can't have a roster, but it is possible that a Roster instance is 167 // retrieved if getRoster() is called *before* connect(). So we have to check here 168 // again if it's an anonymous connection. 169 if (connection.isAnonymous()) 170 return; 171 if (!connection.getConfiguration().isRosterLoadedAtLogin()) 172 return; 173 try { 174 Roster.this.reload(); 175 } 176 catch (SmackException e) { 177 LOGGER.log(Level.SEVERE, "Could not reload Roster", e); 178 return; 179 } 180 } 181 }); 182 } 183 184 /** 185 * Returns the subscription processing mode, which dictates what action 186 * Smack will take when subscription requests from other users are made. 187 * The default subscription mode is {@link SubscriptionMode#accept_all}.<p> 188 * <p/> 189 * If using the manual mode, a PacketListener should be registered that 190 * listens for Presence packets that have a type of 191 * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. 192 * 193 * @return the subscription mode. 194 */ 195 public SubscriptionMode getSubscriptionMode() { 196 return subscriptionMode; 197 } 198 199 /** 200 * Sets the subscription processing mode, which dictates what action 201 * Smack will take when subscription requests from other users are made. 202 * The default subscription mode is {@link SubscriptionMode#accept_all}.<p> 203 * <p/> 204 * If using the manual mode, a PacketListener should be registered that 205 * listens for Presence packets that have a type of 206 * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. 207 * 208 * @param subscriptionMode the subscription mode. 209 */ 210 public void setSubscriptionMode(SubscriptionMode subscriptionMode) { 211 this.subscriptionMode = subscriptionMode; 212 } 213 214 /** 215 * Reloads the entire roster from the server. This is an asynchronous operation, 216 * which means the method will return immediately, and the roster will be 217 * reloaded at a later point when the server responds to the reload request. 218 * @throws NotLoggedInException If not logged in. 219 * @throws NotConnectedException 220 */ 221 public void reload() throws NotLoggedInException, NotConnectedException{ 222 if (!connection.isAuthenticated()) { 223 throw new NotLoggedInException(); 224 } 225 if (connection.isAnonymous()) { 226 throw new IllegalStateException("Anonymous users can't have a roster."); 227 } 228 229 RosterPacket packet = new RosterPacket(); 230 if (rosterStore != null && connection.isRosterVersioningSupported()) { 231 packet.setVersion(rosterStore.getRosterVersion()); 232 } 233 PacketFilter filter = new IQReplyFilter(packet, connection); 234 connection.addPacketListener(new RosterResultListener(), filter); 235 connection.sendPacket(packet); 236 } 237 238 /** 239 * Adds a listener to this roster. The listener will be fired anytime one or more 240 * changes to the roster are pushed from the server. 241 * 242 * @param rosterListener a roster listener. 243 */ 244 public void addRosterListener(RosterListener rosterListener) { 245 if (!rosterListeners.contains(rosterListener)) { 246 rosterListeners.add(rosterListener); 247 } 248 } 249 250 /** 251 * Removes a listener from this roster. The listener will be fired anytime one or more 252 * changes to the roster are pushed from the server. 253 * 254 * @param rosterListener a roster listener. 255 */ 256 public void removeRosterListener(RosterListener rosterListener) { 257 rosterListeners.remove(rosterListener); 258 } 259 260 /** 261 * Creates a new group.<p> 262 * <p/> 263 * Note: you must add at least one entry to the group for the group to be kept 264 * after a logout/login. This is due to the way that XMPP stores group information. 265 * 266 * @param name the name of the group. 267 * @return a new group, or null if the group already exists 268 * @throws IllegalStateException if logged in anonymously 269 */ 270 public RosterGroup createGroup(String name) { 271 if (connection.isAnonymous()) { 272 throw new IllegalStateException("Anonymous users can't have a roster."); 273 } 274 if (groups.containsKey(name)) { 275 return groups.get(name); 276 } 277 278 RosterGroup group = new RosterGroup(name, connection); 279 groups.put(name, group); 280 return group; 281 } 282 283 /** 284 * Creates a new roster entry and presence subscription. The server will asynchronously 285 * update the roster with the subscription status. 286 * 287 * @param user the user. (e.g. johndoe@jabber.org) 288 * @param name the nickname of the user. 289 * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the 290 * the roster entry won't belong to a group. 291 * @throws NoResponseException if there was no response from the server. 292 * @throws XMPPErrorException if an XMPP exception occurs. 293 * @throws NotLoggedInException If not logged in. 294 * @throws NotConnectedException 295 */ 296 public void createEntry(String user, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException { 297 if (!connection.isAuthenticated()) { 298 throw new NotLoggedInException(); 299 } 300 if (connection.isAnonymous()) { 301 throw new IllegalStateException("Anonymous users can't have a roster."); 302 } 303 304 // Create and send roster entry creation packet. 305 RosterPacket rosterPacket = new RosterPacket(); 306 rosterPacket.setType(IQ.Type.SET); 307 RosterPacket.Item item = new RosterPacket.Item(user, name); 308 if (groups != null) { 309 for (String group : groups) { 310 if (group != null && group.trim().length() > 0) { 311 item.addGroupName(group); 312 } 313 } 314 } 315 rosterPacket.addRosterItem(item); 316 connection.createPacketCollectorAndSend(rosterPacket).nextResultOrThrow(); 317 318 // Create a presence subscription packet and send. 319 Presence presencePacket = new Presence(Presence.Type.subscribe); 320 presencePacket.setTo(user); 321 connection.sendPacket(presencePacket); 322 } 323 324 /** 325 * Removes a roster entry from the roster. The roster entry will also be removed from the 326 * unfiled entries or from any roster group where it could belong and will no longer be part 327 * of the roster. Note that this is a synchronous call -- Smack must wait for the server 328 * to send an updated subscription status. 329 * 330 * @param entry a roster entry. 331 * @throws XMPPErrorException if an XMPP error occurs. 332 * @throws NotLoggedInException if not logged in. 333 * @throws NoResponseException SmackException if there was no response from the server. 334 * @throws NotConnectedException 335 * @throws IllegalStateException if connection is not logged in or logged in anonymously 336 */ 337 public void removeEntry(RosterEntry entry) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException { 338 if (!connection.isAuthenticated()) { 339 throw new NotLoggedInException(); 340 } 341 if (connection.isAnonymous()) { 342 throw new IllegalStateException("Anonymous users can't have a roster."); 343 } 344 345 // Only remove the entry if it's in the entry list. 346 // The actual removal logic takes place in RosterPacketListenerprocess>>Packet(Packet) 347 if (!entries.containsKey(entry.getUser())) { 348 return; 349 } 350 RosterPacket packet = new RosterPacket(); 351 packet.setType(IQ.Type.SET); 352 RosterPacket.Item item = RosterEntry.toRosterItem(entry); 353 // Set the item type as REMOVE so that the server will delete the entry 354 item.setItemType(RosterPacket.ItemType.remove); 355 packet.addRosterItem(item); 356 connection.createPacketCollectorAndSend(packet).nextResultOrThrow(); 357 } 358 359 /** 360 * Returns a count of the entries in the roster. 361 * 362 * @return the number of entries in the roster. 363 */ 364 public int getEntryCount() { 365 return getEntries().size(); 366 } 367 368 /** 369 * Returns an unmodifiable collection of all entries in the roster, including entries 370 * that don't belong to any groups. 371 * 372 * @return all entries in the roster. 373 */ 374 public Collection<RosterEntry> getEntries() { 375 Set<RosterEntry> allEntries = new HashSet<RosterEntry>(); 376 // Loop through all roster groups and add their entries to the answer 377 for (RosterGroup rosterGroup : getGroups()) { 378 allEntries.addAll(rosterGroup.getEntries()); 379 } 380 // Add the roster unfiled entries to the answer 381 allEntries.addAll(unfiledEntries); 382 383 return Collections.unmodifiableCollection(allEntries); 384 } 385 386 /** 387 * Returns a count of the unfiled entries in the roster. An unfiled entry is 388 * an entry that doesn't belong to any groups. 389 * 390 * @return the number of unfiled entries in the roster. 391 */ 392 public int getUnfiledEntryCount() { 393 return unfiledEntries.size(); 394 } 395 396 /** 397 * Returns an unmodifiable collection for the unfiled roster entries. An unfiled entry is 398 * an entry that doesn't belong to any groups. 399 * 400 * @return the unfiled roster entries. 401 */ 402 public Collection<RosterEntry> getUnfiledEntries() { 403 return Collections.unmodifiableList(unfiledEntries); 404 } 405 406 /** 407 * Returns the roster entry associated with the given XMPP address or 408 * <tt>null</tt> if the user is not an entry in the roster. 409 * 410 * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be 411 * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource"). 412 * @return the roster entry or <tt>null</tt> if it does not exist. 413 */ 414 public RosterEntry getEntry(String user) { 415 if (user == null) { 416 return null; 417 } 418 return entries.get(user.toLowerCase(Locale.US)); 419 } 420 421 /** 422 * Returns true if the specified XMPP address is an entry in the roster. 423 * 424 * @param user the XMPP address of the user (eg "jsmith@example.com"). The 425 * address could be in any valid format (e.g. "domain/resource", 426 * "user@domain" or "user@domain/resource"). 427 * @return true if the XMPP address is an entry in the roster. 428 */ 429 public boolean contains(String user) { 430 return getEntry(user) != null; 431 } 432 433 /** 434 * Returns the roster group with the specified name, or <tt>null</tt> if the 435 * group doesn't exist. 436 * 437 * @param name the name of the group. 438 * @return the roster group with the specified name. 439 */ 440 public RosterGroup getGroup(String name) { 441 return groups.get(name); 442 } 443 444 /** 445 * Returns the number of the groups in the roster. 446 * 447 * @return the number of groups in the roster. 448 */ 449 public int getGroupCount() { 450 return groups.size(); 451 } 452 453 /** 454 * Returns an unmodifiable collections of all the roster groups. 455 * 456 * @return an iterator for all roster groups. 457 */ 458 public Collection<RosterGroup> getGroups() { 459 return Collections.unmodifiableCollection(groups.values()); 460 } 461 462 /** 463 * Returns the presence info for a particular user. If the user is offline, or 464 * if no presence data is available (such as when you are not subscribed to the 465 * user's presence updates), unavailable presence will be returned.<p> 466 * <p/> 467 * If the user has several presences (one for each resource), then the presence with 468 * highest priority will be returned. If multiple presences have the same priority, 469 * the one with the "most available" presence mode will be returned. In order, 470 * that's {@link org.jivesoftware.smack.packet.Presence.Mode#chat free to chat}, 471 * {@link org.jivesoftware.smack.packet.Presence.Mode#available available}, 472 * {@link org.jivesoftware.smack.packet.Presence.Mode#away away}, 473 * {@link org.jivesoftware.smack.packet.Presence.Mode#xa extended away}, and 474 * {@link org.jivesoftware.smack.packet.Presence.Mode#dnd do not disturb}.<p> 475 * <p/> 476 * Note that presence information is received asynchronously. So, just after logging 477 * in to the server, presence values for users in the roster may be unavailable 478 * even if they are actually online. In other words, the value returned by this 479 * method should only be treated as a snapshot in time, and may not accurately reflect 480 * other user's presence instant by instant. If you need to track presence over time, 481 * such as when showing a visual representation of the roster, consider using a 482 * {@link RosterListener}. 483 * 484 * @param user an XMPP ID. The address could be in any valid format (e.g. 485 * "domain/resource", "user@domain" or "user@domain/resource"). Any resource 486 * information that's part of the ID will be discarded. 487 * @return the user's current presence, or unavailable presence if the user is offline 488 * or if no presence information is available.. 489 */ 490 public Presence getPresence(String user) { 491 String key = getPresenceMapKey(StringUtils.parseBareAddress(user)); 492 Map<String, Presence> userPresences = presenceMap.get(key); 493 if (userPresences == null) { 494 Presence presence = new Presence(Presence.Type.unavailable); 495 presence.setFrom(user); 496 return presence; 497 } 498 else { 499 // Find the resource with the highest priority 500 // Might be changed to use the resource with the highest availability instead. 501 Presence presence = null; 502 503 for (String resource : userPresences.keySet()) { 504 Presence p = userPresences.get(resource); 505 if (!p.isAvailable()) { 506 continue; 507 } 508 // Chose presence with highest priority first. 509 if (presence == null || p.getPriority() > presence.getPriority()) { 510 presence = p; 511 } 512 // If equal priority, choose "most available" by the mode value. 513 else if (p.getPriority() == presence.getPriority()) { 514 Presence.Mode pMode = p.getMode(); 515 // Default to presence mode of available. 516 if (pMode == null) { 517 pMode = Presence.Mode.available; 518 } 519 Presence.Mode presenceMode = presence.getMode(); 520 // Default to presence mode of available. 521 if (presenceMode == null) { 522 presenceMode = Presence.Mode.available; 523 } 524 if (pMode.compareTo(presenceMode) < 0) { 525 presence = p; 526 } 527 } 528 } 529 if (presence == null) { 530 presence = new Presence(Presence.Type.unavailable); 531 presence.setFrom(user); 532 return presence; 533 } 534 else { 535 return presence; 536 } 537 } 538 } 539 540 /** 541 * Returns the presence info for a particular user's resource, or unavailable presence 542 * if the user is offline or if no presence information is available, such as 543 * when you are not subscribed to the user's presence updates. 544 * 545 * @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource). 546 * @return the user's current presence, or unavailable presence if the user is offline 547 * or if no presence information is available. 548 */ 549 public Presence getPresenceResource(String userWithResource) { 550 String key = getPresenceMapKey(userWithResource); 551 String resource = StringUtils.parseResource(userWithResource); 552 Map<String, Presence> userPresences = presenceMap.get(key); 553 if (userPresences == null) { 554 Presence presence = new Presence(Presence.Type.unavailable); 555 presence.setFrom(userWithResource); 556 return presence; 557 } 558 else { 559 Presence presence = userPresences.get(resource); 560 if (presence == null) { 561 presence = new Presence(Presence.Type.unavailable); 562 presence.setFrom(userWithResource); 563 return presence; 564 } 565 else { 566 return presence; 567 } 568 } 569 } 570 571 /** 572 * Returns a List of Presence objects for all of a user's current presences 573 * or an unavailable presence if the user is unavailable (offline) or if no presence 574 * information is available, such as when you are not subscribed to the user's presence 575 * updates. 576 * 577 * @param user a XMPP ID, e.g. jdoe@example.com. 578 * @return a List of Presence objects for all the user's current presences, 579 * or an unavailable presence if the user is offline or if no presence information 580 * is available. 581 */ 582 public List<Presence> getPresences(String user) { 583 List<Presence> res; 584 String key = getPresenceMapKey(user); 585 Map<String, Presence> userPresences = presenceMap.get(key); 586 if (userPresences == null) { 587 Presence presence = new Presence(Presence.Type.unavailable); 588 presence.setFrom(user); 589 res = Arrays.asList(presence); 590 } 591 else { 592 List<Presence> answer = new ArrayList<Presence>(); 593 for (Presence presence : userPresences.values()) { 594 if (presence.isAvailable()) { 595 answer.add(presence); 596 } 597 } 598 if (!answer.isEmpty()) { 599 res = answer; 600 } 601 else { 602 Presence presence = new Presence(Presence.Type.unavailable); 603 presence.setFrom(user); 604 res = Arrays.asList(presence); 605 } 606 } 607 return Collections.unmodifiableList(res); 608 } 609 610 /** 611 * Returns the key to use in the presenceMap for a fully qualified XMPP ID. 612 * The roster can contain any valid address format such us "domain/resource", 613 * "user@domain" or "user@domain/resource". If the roster contains an entry 614 * associated with the fully qualified XMPP ID then use the fully qualified XMPP 615 * ID as the key in presenceMap, otherwise use the bare address. Note: When the 616 * key in presenceMap is a fully qualified XMPP ID, the userPresences is useless 617 * since it will always contain one entry for the user. 618 * 619 * @param user the bare or fully qualified XMPP ID, e.g. jdoe@example.com or 620 * jdoe@example.com/Work. 621 * @return the key to use in the presenceMap for the fully qualified XMPP ID. 622 */ 623 private String getPresenceMapKey(String user) { 624 if (user == null) { 625 return null; 626 } 627 String key = user; 628 if (!contains(user)) { 629 key = StringUtils.parseBareAddress(user); 630 } 631 return key.toLowerCase(Locale.US); 632 } 633 634 /** 635 * Changes the presence of available contacts offline by simulating an unavailable 636 * presence sent from the server. After a disconnection, every Presence is set 637 * to offline. 638 * @throws NotConnectedException 639 */ 640 private void setOfflinePresences() throws NotConnectedException { 641 Presence packetUnavailable; 642 for (String user : presenceMap.keySet()) { 643 Map<String, Presence> resources = presenceMap.get(user); 644 if (resources != null) { 645 for (String resource : resources.keySet()) { 646 packetUnavailable = new Presence(Presence.Type.unavailable); 647 packetUnavailable.setFrom(user + "/" + resource); 648 presencePacketListener.processPacket(packetUnavailable); 649 } 650 } 651 } 652 } 653 654 /** 655 * Fires roster changed event to roster listeners indicating that the 656 * specified collections of contacts have been added, updated or deleted 657 * from the roster. 658 * 659 * @param addedEntries the collection of address of the added contacts. 660 * @param updatedEntries the collection of address of the updated contacts. 661 * @param deletedEntries the collection of address of the deleted contacts. 662 */ 663 private void fireRosterChangedEvent(Collection<String> addedEntries, Collection<String> updatedEntries, 664 Collection<String> deletedEntries) { 665 for (RosterListener listener : rosterListeners) { 666 if (!addedEntries.isEmpty()) { 667 listener.entriesAdded(addedEntries); 668 } 669 if (!updatedEntries.isEmpty()) { 670 listener.entriesUpdated(updatedEntries); 671 } 672 if (!deletedEntries.isEmpty()) { 673 listener.entriesDeleted(deletedEntries); 674 } 675 } 676 } 677 678 /** 679 * Fires roster presence changed event to roster listeners. 680 * 681 * @param presence the presence change. 682 */ 683 private void fireRosterPresenceEvent(Presence presence) { 684 for (RosterListener listener : rosterListeners) { 685 listener.presenceChanged(presence); 686 } 687 } 688 689 private void addUpdateEntry(Collection<String> addedEntries, Collection<String> updatedEntries, 690 Collection<String> unchangedEntries, RosterPacket.Item item, RosterEntry entry) { 691 RosterEntry oldEntry = entries.put(item.getUser(), entry); 692 if (oldEntry == null) { 693 addedEntries.add(item.getUser()); 694 } 695 else { 696 RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry); 697 if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) { 698 updatedEntries.add(item.getUser()); 699 } else { 700 // Record the entry as unchanged, so that it doesn't end up as deleted entry 701 unchangedEntries.add(item.getUser()); 702 } 703 } 704 705 // Mark the entry as unfiled if it does not belong to any groups. 706 if (item.getGroupNames().isEmpty()) { 707 unfiledEntries.remove(entry); 708 unfiledEntries.add(entry); 709 } 710 else { 711 unfiledEntries.remove(entry); 712 } 713 714 // Add the user to the new groups 715 716 // Add the entry to the groups 717 List<String> newGroupNames = new ArrayList<String>(); 718 for (String groupName : item.getGroupNames()) { 719 // Add the group name to the list. 720 newGroupNames.add(groupName); 721 722 // Add the entry to the group. 723 RosterGroup group = getGroup(groupName); 724 if (group == null) { 725 group = createGroup(groupName); 726 groups.put(groupName, group); 727 } 728 // Add the entry. 729 group.addEntryLocal(entry); 730 } 731 732 // Remove user from the remaining groups. 733 List<String> oldGroupNames = new ArrayList<String>(); 734 for (RosterGroup group: getGroups()) { 735 oldGroupNames.add(group.getName()); 736 } 737 oldGroupNames.removeAll(newGroupNames); 738 739 for (String groupName : oldGroupNames) { 740 RosterGroup group = getGroup(groupName); 741 group.removeEntryLocal(entry); 742 if (group.getEntryCount() == 0) { 743 groups.remove(groupName); 744 } 745 } 746 } 747 748 private void deleteEntry(Collection<String> deletedEntries, RosterEntry entry) { 749 String user = entry.getUser(); 750 entries.remove(user); 751 unfiledEntries.remove(entry); 752 presenceMap.remove(StringUtils.parseBareAddress(user)); 753 deletedEntries.add(user); 754 755 for (Entry<String,RosterGroup> e: groups.entrySet()) { 756 RosterGroup group = e.getValue(); 757 group.removeEntryLocal(entry); 758 if (group.getEntryCount() == 0) { 759 groups.remove(e.getKey()); 760 } 761 } 762 } 763 764 765 /** 766 * Removes all the groups with no entries. 767 * 768 * This is used by {@link RosterPushListener} and {@link RosterResultListener} to 769 * cleanup groups after removing contacts. 770 */ 771 private void removeEmptyGroups() { 772 // We have to do this because RosterGroup.removeEntry removes the entry immediately 773 // (locally) and the group could remain empty. 774 // TODO Check the performance/logic for rosters with large number of groups 775 for (RosterGroup group : getGroups()) { 776 if (group.getEntryCount() == 0) { 777 groups.remove(group.getName()); 778 } 779 } 780 } 781 782 /** 783 * Ignore ItemTypes as of RFC 6121, 2.1.2.5. 784 * 785 * This is used by {@link RosterPushListener} and {@link RosterResultListener}. 786 * */ 787 private static boolean hasValidSubscriptionType(RosterPacket.Item item) { 788 return item.getItemType().equals(RosterPacket.ItemType.none) 789 || item.getItemType().equals(RosterPacket.ItemType.from) 790 || item.getItemType().equals(RosterPacket.ItemType.to) 791 || item.getItemType().equals(RosterPacket.ItemType.both); 792 } 793 794 /** 795 * An enumeration for the subscription mode options. 796 */ 797 public enum SubscriptionMode { 798 799 /** 800 * Automatically accept all subscription and unsubscription requests. This is 801 * the default mode and is suitable for simple client. More complex client will 802 * likely wish to handle subscription requests manually. 803 */ 804 accept_all, 805 806 /** 807 * Automatically reject all subscription requests. 808 */ 809 reject_all, 810 811 /** 812 * Subscription requests are ignored, which means they must be manually 813 * processed by registering a listener for presence packets and then looking 814 * for any presence requests that have the type Presence.Type.SUBSCRIBE or 815 * Presence.Type.UNSUBSCRIBE. 816 */ 817 manual 818 } 819 820 /** 821 * Listens for all presence packets and processes them. 822 */ 823 private class PresencePacketListener implements PacketListener { 824 825 public void processPacket(Packet packet) throws NotConnectedException { 826 Presence presence = (Presence) packet; 827 String from = presence.getFrom(); 828 String key = getPresenceMapKey(from); 829 830 // If an "available" presence, add it to the presence map. Each presence 831 // map will hold for a particular user a map with the presence 832 // packets saved for each resource. 833 if (presence.getType() == Presence.Type.available) { 834 Map<String, Presence> userPresences; 835 // Get the user presence map 836 if (presenceMap.get(key) == null) { 837 userPresences = new ConcurrentHashMap<String, Presence>(); 838 presenceMap.put(key, userPresences); 839 } 840 else { 841 userPresences = presenceMap.get(key); 842 } 843 // See if an offline presence was being stored in the map. If so, remove 844 // it since we now have an online presence. 845 userPresences.remove(""); 846 // Add the new presence, using the resources as a key. 847 userPresences.put(StringUtils.parseResource(from), presence); 848 // If the user is in the roster, fire an event. 849 RosterEntry entry = entries.get(key); 850 if (entry != null) { 851 fireRosterPresenceEvent(presence); 852 } 853 } 854 // If an "unavailable" packet. 855 else if (presence.getType() == Presence.Type.unavailable) { 856 // If no resource, this is likely an offline presence as part of 857 // a roster presence flood. In that case, we store it. 858 if ("".equals(StringUtils.parseResource(from))) { 859 Map<String, Presence> userPresences; 860 // Get the user presence map 861 if (presenceMap.get(key) == null) { 862 userPresences = new ConcurrentHashMap<String, Presence>(); 863 presenceMap.put(key, userPresences); 864 } 865 else { 866 userPresences = presenceMap.get(key); 867 } 868 userPresences.put("", presence); 869 } 870 // Otherwise, this is a normal offline presence. 871 else if (presenceMap.get(key) != null) { 872 Map<String, Presence> userPresences = presenceMap.get(key); 873 // Store the offline presence, as it may include extra information 874 // such as the user being on vacation. 875 userPresences.put(StringUtils.parseResource(from), presence); 876 } 877 // If the user is in the roster, fire an event. 878 RosterEntry entry = entries.get(key); 879 if (entry != null) { 880 fireRosterPresenceEvent(presence); 881 } 882 } 883 else if (presence.getType() == Presence.Type.subscribe) { 884 Presence response = null; 885 switch (subscriptionMode) { 886 case accept_all: 887 // Accept all subscription requests. 888 response = new Presence(Presence.Type.subscribed); 889 break; 890 case reject_all: 891 // Reject all subscription requests. 892 response = new Presence(Presence.Type.unsubscribed); 893 break; 894 case manual: 895 default: 896 // Otherwise, in manual mode so ignore. 897 break; 898 } 899 if (response != null) { 900 response.setTo(presence.getFrom()); 901 connection.sendPacket(response); 902 } 903 } 904 else if (presence.getType() == Presence.Type.unsubscribe) { 905 if (subscriptionMode != SubscriptionMode.manual) { 906 // Acknowledge and accept unsubscription notification so that the 907 // server will stop sending notifications saying that the contact 908 // has unsubscribed to our presence. 909 Presence response = new Presence(Presence.Type.unsubscribed); 910 response.setTo(presence.getFrom()); 911 connection.sendPacket(response); 912 } 913 // Otherwise, in manual mode so ignore. 914 } 915 // Error presence packets from a bare JID mean we invalidate all existing 916 // presence info for the user. 917 else if (presence.getType() == Presence.Type.error && 918 "".equals(StringUtils.parseResource(from))) 919 { 920 Map<String, Presence> userPresences; 921 if (!presenceMap.containsKey(key)) { 922 userPresences = new ConcurrentHashMap<String, Presence>(); 923 presenceMap.put(key, userPresences); 924 } 925 else { 926 userPresences = presenceMap.get(key); 927 // Any other presence data is invalidated by the error packet. 928 userPresences.clear(); 929 } 930 // Set the new presence using the empty resource as a key. 931 userPresences.put("", presence); 932 // If the user is in the roster, fire an event. 933 RosterEntry entry = entries.get(key); 934 if (entry != null) { 935 fireRosterPresenceEvent(presence); 936 } 937 } 938 } 939 } 940 941 /** 942 * Handles the case of the empty IQ-result for roster versioning. 943 * 944 * Intended to listen for a concrete roster result and deregisters 945 * itself after a processed packet. 946 */ 947 private class RosterResultListener implements PacketListener { 948 949 @Override 950 public void processPacket(Packet packet) { 951 connection.removePacketListener(this); 952 953 IQ result = (IQ)packet; 954 if (!result.getType().equals(IQ.Type.RESULT)) { 955 LOGGER.severe("Roster result IQ not of type result. Packet: " + result.toXML()); 956 return; 957 } 958 959 Collection<String> addedEntries = new ArrayList<String>(); 960 Collection<String> updatedEntries = new ArrayList<String>(); 961 Collection<String> deletedEntries = new ArrayList<String>(); 962 Collection<String> unchangedEntries = new ArrayList<String>(); 963 964 if (packet instanceof RosterPacket) { 965 // Non-empty roster result. This stanza contains all the roster elements. 966 RosterPacket rosterPacket = (RosterPacket) packet; 967 968 String version = rosterPacket.getVersion(); 969 970 // Ignore items without valid subscription type 971 ArrayList<Item> validItems = new ArrayList<RosterPacket.Item>(); 972 for (RosterPacket.Item item : rosterPacket.getRosterItems()) { 973 if (hasValidSubscriptionType(item)) { 974 validItems.add(item); 975 } 976 } 977 978 for (RosterPacket.Item item : validItems) { 979 RosterEntry entry = new RosterEntry(item.getUser(), item.getName(), 980 item.getItemType(), item.getItemStatus(), Roster.this, connection); 981 addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry); 982 } 983 984 // Delete all entries which where not added or updated 985 Set<String> toDelete = new HashSet<String>(); 986 for (RosterEntry entry : entries.values()) { 987 toDelete.add(entry.getUser()); 988 } 989 toDelete.removeAll(addedEntries); 990 toDelete.removeAll(updatedEntries); 991 toDelete.removeAll(unchangedEntries); 992 for (String user : toDelete) { 993 deleteEntry(deletedEntries, entries.get(user)); 994 } 995 996 if (rosterStore != null) { 997 rosterStore.resetEntries(validItems, version); 998 } 999 1000 removeEmptyGroups(); 1001 } 1002 else { 1003 // Empty roster result as defined in RFC6121 2.6.3. An empty roster result basically 1004 // means that rosterver was used and the roster hasn't changed (much) since the 1005 // version we presented the server. So we simply load the roster from the store and 1006 // await possible further roster pushes. 1007 for (RosterPacket.Item item : rosterStore.getEntries()) { 1008 RosterEntry entry = new RosterEntry(item.getUser(), item.getName(), 1009 item.getItemType(), item.getItemStatus(), Roster.this, connection); 1010 addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry); 1011 } 1012 } 1013 1014 rosterInitialized = true; 1015 synchronized (Roster.this) { 1016 Roster.this.notifyAll(); 1017 } 1018 // Fire event for roster listeners. 1019 fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); 1020 } 1021 } 1022 1023 /** 1024 * Listens for all roster pushes and processes them. 1025 */ 1026 private class RosterPushListener implements PacketListener { 1027 1028 public void processPacket(Packet packet) throws NotConnectedException { 1029 RosterPacket rosterPacket = (RosterPacket) packet; 1030 1031 String version = rosterPacket.getVersion(); 1032 1033 // Roster push (RFC 6121, 2.1.6) 1034 // A roster push with a non-empty from not matching our address MUST be ignored 1035 String jid = StringUtils.parseBareAddress(connection.getUser()); 1036 if (rosterPacket.getFrom() != null && 1037 !rosterPacket.getFrom().equals(jid)) { 1038 LOGGER.warning("Ignoring roster push with a non matching 'from' ourJid=" + jid 1039 + " from=" + rosterPacket.getFrom()); 1040 return; 1041 } 1042 1043 // A roster push must contain exactly one entry 1044 Collection<Item> items = rosterPacket.getRosterItems(); 1045 if (items.size() != 1) { 1046 LOGGER.warning("Ignoring roster push with not exaclty one entry. size=" + items.size()); 1047 return; 1048 } 1049 1050 Collection<String> addedEntries = new ArrayList<String>(); 1051 Collection<String> updatedEntries = new ArrayList<String>(); 1052 Collection<String> deletedEntries = new ArrayList<String>(); 1053 Collection<String> unchangedEntries = new ArrayList<String>(); 1054 1055 // We assured abouve that the size of items is exaclty 1, therefore we are able to 1056 // safely retrieve this single item here. 1057 Item item = items.iterator().next(); 1058 RosterEntry entry = new RosterEntry(item.getUser(), item.getName(), 1059 item.getItemType(), item.getItemStatus(), Roster.this, connection); 1060 1061 if (item.getItemType().equals(RosterPacket.ItemType.remove)) { 1062 deleteEntry(deletedEntries, entry); 1063 if (rosterStore != null) { 1064 rosterStore.removeEntry(entry.getUser(), version); 1065 } 1066 } 1067 else if (hasValidSubscriptionType(item)) { 1068 addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry); 1069 if (rosterStore != null) { 1070 rosterStore.addEntry(item, version); 1071 } 1072 } 1073 connection.sendPacket(IQ.createResultIQ(rosterPacket)); 1074 1075 removeEmptyGroups(); 1076 1077 // Fire event for roster listeners. 1078 fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); 1079 } 1080 } 1081}