1 /** 2 * $RCSfile$ 3 * $Revision$ 4 * $Date$ 5 * 6 * Copyright 2003-2007 Jive Software. 7 * 8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 9 * you may not use this file except in compliance with the License. 10 * You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, software 15 * distributed under the License is distributed on an "AS IS" BASIS, 16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 * See the License for the specific language governing permissions and 18 * limitations under the License. 19 */ 20 21 package org.jivesoftware.smack; 22 23 import org.jivesoftware.smack.filter.PacketFilter; 24 import org.jivesoftware.smack.filter.PacketIDFilter; 25 import org.jivesoftware.smack.filter.PacketTypeFilter; 26 import org.jivesoftware.smack.packet.IQ; 27 import org.jivesoftware.smack.packet.Packet; 28 import org.jivesoftware.smack.packet.Presence; 29 import org.jivesoftware.smack.packet.RosterPacket; 30 import org.jivesoftware.smack.util.StringUtils; 31 32 import java.util.*; 33 import java.util.concurrent.ConcurrentHashMap; 34 import java.util.concurrent.CopyOnWriteArrayList; 35 36 /** 37 * Represents a user's roster, which is the collection of users a person receives 38 * presence updates for. Roster items are categorized into groups for easier management.<p> 39 * <p/> 40 * Others users may attempt to subscribe to this user using a subscription request. Three 41 * modes are supported for handling these requests: <ul> 42 * <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li> 43 * <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li> 44 * <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li> 45 * </ul> 46 * 47 * @author Matt Tucker 48 * @see Connection#getRoster() 49 */ 50 public class Roster { 51 52 /** 53 * The default subscription processing mode to use when a Roster is created. By default 54 * all subscription requests are automatically accepted. 55 */ 56 private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.accept_all; 57 private RosterStorage persistentStorage; 58 59 private Connection connection; 60 private final Map<String, RosterGroup> groups; 61 private final Map<String,RosterEntry> entries; 62 private final List<RosterEntry> unfiledEntries; 63 private final List<RosterListener> rosterListeners; 64 private Map<String, Map<String, Presence>> presenceMap; 65 // The roster is marked as initialized when at least a single roster packet 66 // has been received and processed. 67 boolean rosterInitialized = false; 68 private PresencePacketListener presencePacketListener; 69 70 private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode(); 71 72 private String requestPacketId; 73 74 /** 75 * Returns the default subscription processing mode to use when a new Roster is created. The 76 * subscription processing mode dictates what action Smack will take when subscription 77 * requests from other users are made. The default subscription mode 78 * is {@link SubscriptionMode#accept_all}. 79 * 80 * @return the default subscription mode to use for new Rosters 81 */ 82 public static SubscriptionMode getDefaultSubscriptionMode() { 83 return defaultSubscriptionMode; 84 } 85 86 /** 87 * Sets the default subscription processing mode to use when a new Roster is created. The 88 * subscription processing mode dictates what action Smack will take when subscription 89 * requests from other users are made. The default subscription mode 90 * is {@link SubscriptionMode#accept_all}. 91 * 92 * @param subscriptionMode the default subscription mode to use for new Rosters. 93 */ 94 public static void setDefaultSubscriptionMode(SubscriptionMode subscriptionMode) { 95 defaultSubscriptionMode = subscriptionMode; 96 } 97 98 Roster(final Connection connection, RosterStorage persistentStorage){ 99 this(connection); 100 this.persistentStorage = persistentStorage; 101 } 102 103 /** 104 * Creates a new roster. 105 * 106 * @param connection an XMPP connection. 107 */ 108 Roster(final Connection connection) { 109 this.connection = connection; 110 //Disable roster versioning if server doesn't offer support for it 111 if(!connection.getConfiguration().isRosterVersioningAvailable()){ 112 persistentStorage=null; 113 } 114 groups = new ConcurrentHashMap<String, RosterGroup>(); 115 unfiledEntries = new CopyOnWriteArrayList<RosterEntry>(); 116 entries = new ConcurrentHashMap<String,RosterEntry>(); 117 rosterListeners = new CopyOnWriteArrayList<RosterListener>(); 118 presenceMap = new ConcurrentHashMap<String, Map<String, Presence>>(); 119 // Listen for any roster packets. 120 PacketFilter rosterFilter = new PacketTypeFilter(RosterPacket.class); 121 connection.addPacketListener(new RosterPacketListener(), rosterFilter); 122 // Listen for any presence packets. 123 PacketFilter presenceFilter = new PacketTypeFilter(Presence.class); 124 presencePacketListener = new PresencePacketListener(); 125 connection.addPacketListener(presencePacketListener, presenceFilter); 126 127 // Listen for connection events 128 final ConnectionListener connectionListener = new AbstractConnectionListener() { 129 130 public void connectionClosed() { 131 // Changes the presence available contacts to unavailable 132 setOfflinePresences(); 133 } 134 135 public void connectionClosedOnError(Exception e) { 136 // Changes the presence available contacts to unavailable 137 setOfflinePresences(); 138 } 139 140 }; 141 142 // if not connected add listener after successful login 143 if(!this.connection.isConnected()) { 144 Connection.addConnectionCreationListener(new ConnectionCreationListener() { 145 146 public void connectionCreated(Connection connection) { 147 if(connection.equals(Roster.this.connection)) { 148 Roster.this.connection.addConnectionListener(connectionListener); 149 } 150 151 } 152 }); 153 } else { 154 connection.addConnectionListener(connectionListener); 155 } 156 } 157 158 /** 159 * Returns the subscription processing mode, which dictates what action 160 * Smack will take when subscription requests from other users are made. 161 * The default subscription mode is {@link SubscriptionMode#accept_all}.<p> 162 * <p/> 163 * If using the manual mode, a PacketListener should be registered that 164 * listens for Presence packets that have a type of 165 * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. 166 * 167 * @return the subscription mode. 168 */ 169 public SubscriptionMode getSubscriptionMode() { 170 return subscriptionMode; 171 } 172 173 /** 174 * Sets the subscription processing mode, which dictates what action 175 * Smack will take when subscription requests from other users are made. 176 * The default subscription mode is {@link SubscriptionMode#accept_all}.<p> 177 * <p/> 178 * If using the manual mode, a PacketListener should be registered that 179 * listens for Presence packets that have a type of 180 * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}. 181 * 182 * @param subscriptionMode the subscription mode. 183 */ 184 public void setSubscriptionMode(SubscriptionMode subscriptionMode) { 185 this.subscriptionMode = subscriptionMode; 186 } 187 188 /** 189 * Reloads the entire roster from the server. This is an asynchronous operation, 190 * which means the method will return immediately, and the roster will be 191 * reloaded at a later point when the server responds to the reload request. 192 * 193 * @throws IllegalStateException if connection is not logged in or logged in anonymously 194 */ 195 public void reload() { 196 if (!connection.isAuthenticated()) { 197 throw new IllegalStateException("Not logged in to server."); 198 } 199 if (connection.isAnonymous()) { 200 throw new IllegalStateException("Anonymous users can't have a roster."); 201 } 202 203 RosterPacket packet = new RosterPacket(); 204 if(persistentStorage!=null){ 205 packet.setVersion(persistentStorage.getRosterVersion()); 206 } 207 requestPacketId = packet.getPacketID(); 208 PacketFilter idFilter = new PacketIDFilter(requestPacketId); 209 connection.addPacketListener(new RosterResultListener(), idFilter); 210 connection.sendPacket(packet); 211 } 212 213 /** 214 * Adds a listener to this roster. The listener will be fired anytime one or more 215 * changes to the roster are pushed from the server. 216 * 217 * @param rosterListener a roster listener. 218 */ 219 public void addRosterListener(RosterListener rosterListener) { 220 if (!rosterListeners.contains(rosterListener)) { 221 rosterListeners.add(rosterListener); 222 } 223 } 224 225 /** 226 * Removes a listener from this roster. The listener will be fired anytime one or more 227 * changes to the roster are pushed from the server. 228 * 229 * @param rosterListener a roster listener. 230 */ 231 public void removeRosterListener(RosterListener rosterListener) { 232 rosterListeners.remove(rosterListener); 233 } 234 235 /** 236 * Creates a new group.<p> 237 * <p/> 238 * Note: you must add at least one entry to the group for the group to be kept 239 * after a logout/login. This is due to the way that XMPP stores group information. 240 * 241 * @param name the name of the group. 242 * @return a new group. 243 * @throws IllegalStateException if connection is not logged in or logged in anonymously 244 */ 245 public RosterGroup createGroup(String name) { 246 if (!connection.isAuthenticated()) { 247 throw new IllegalStateException("Not logged in to server."); 248 } 249 if (connection.isAnonymous()) { 250 throw new IllegalStateException("Anonymous users can't have a roster."); 251 } 252 if (groups.containsKey(name)) { 253 throw new IllegalArgumentException("Group with name " + name + " alread exists."); 254 } 255 256 RosterGroup group = new RosterGroup(name, connection); 257 groups.put(name, group); 258 return group; 259 } 260 261 /** 262 * Creates a new roster entry and presence subscription. The server will asynchronously 263 * update the roster with the subscription status. 264 * 265 * @param user the user. (e.g. johndoe (at) jabber.org) 266 * @param name the nickname of the user. 267 * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the 268 * the roster entry won't belong to a group. 269 * @throws XMPPException if an XMPP exception occurs. 270 * @throws IllegalStateException if connection is not logged in or logged in anonymously 271 */ 272 public void createEntry(String user, String name, String[] groups) throws XMPPException { 273 if (!connection.isAuthenticated()) { 274 throw new IllegalStateException("Not logged in to server."); 275 } 276 if (connection.isAnonymous()) { 277 throw new IllegalStateException("Anonymous users can't have a roster."); 278 } 279 280 // Create and send roster entry creation packet. 281 RosterPacket rosterPacket = new RosterPacket(); 282 rosterPacket.setType(IQ.Type.SET); 283 RosterPacket.Item item = new RosterPacket.Item(user, name); 284 if (groups != null) { 285 for (String group : groups) { 286 if (group != null && group.trim().length() > 0) { 287 item.addGroupName(group); 288 } 289 } 290 } 291 rosterPacket.addRosterItem(item); 292 // Wait up to a certain number of seconds for a reply from the server. 293 PacketCollector collector = connection.createPacketCollector( 294 new PacketIDFilter(rosterPacket.getPacketID())); 295 connection.sendPacket(rosterPacket); 296 IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); 297 collector.cancel(); 298 if (response == null) { 299 throw new XMPPException("No response from the server."); 300 } 301 // If the server replied with an error, throw an exception. 302 else if (response.getType() == IQ.Type.ERROR) { 303 throw new XMPPException(response.getError()); 304 } 305 306 // Create a presence subscription packet and send. 307 Presence presencePacket = new Presence(Presence.Type.subscribe); 308 presencePacket.setTo(user); 309 connection.sendPacket(presencePacket); 310 } 311 312 private void insertRosterItems(List<RosterPacket.Item> items){ 313 Collection<String> addedEntries = new ArrayList<String>(); 314 Collection<String> updatedEntries = new ArrayList<String>(); 315 Collection<String> deletedEntries = new ArrayList<String>(); 316 Iterator<RosterPacket.Item> iter = items.iterator(); 317 while(iter.hasNext()){ 318 insertRosterItem(iter.next(), addedEntries,updatedEntries,deletedEntries); 319 } 320 fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); 321 } 322 323 private void insertRosterItem(RosterPacket.Item item, Collection<String> addedEntries, 324 Collection<String> updatedEntries, Collection<String> deletedEntries){ 325 RosterEntry entry = new RosterEntry(item.getUser(), item.getName(), 326 item.getItemType(), item.getItemStatus(), this, connection); 327 328 // If the packet is of the type REMOVE then remove the entry 329 if (RosterPacket.ItemType.remove.equals(item.getItemType())) { 330 // Remove the entry from the entry list. 331 if (entries.containsKey(item.getUser())) { 332 entries.remove(item.getUser()); 333 } 334 // Remove the entry from the unfiled entry list. 335 if (unfiledEntries.contains(entry)) { 336 unfiledEntries.remove(entry); 337 } 338 // Removing the user from the roster, so remove any presence information 339 // about them. 340 String key = StringUtils.parseName(item.getUser()) + "@" + 341 StringUtils.parseServer(item.getUser()); 342 presenceMap.remove(key); 343 // Keep note that an entry has been removed 344 if(deletedEntries!=null){ 345 deletedEntries.add(item.getUser()); 346 } 347 } 348 else { 349 // Make sure the entry is in the entry list. 350 if (!entries.containsKey(item.getUser())) { 351 entries.put(item.getUser(), entry); 352 // Keep note that an entry has been added 353 if(addedEntries!=null){ 354 addedEntries.add(item.getUser()); 355 } 356 } 357 else { 358 // If the entry was in then list then update its state with the new values 359 entries.put(item.getUser(), entry); 360 361 // Keep note that an entry has been updated 362 if(updatedEntries!=null){ 363 updatedEntries.add(item.getUser()); 364 } 365 } 366 // If the roster entry belongs to any groups, remove it from the 367 // list of unfiled entries. 368 if (!item.getGroupNames().isEmpty()) { 369 unfiledEntries.remove(entry); 370 } 371 // Otherwise add it to the list of unfiled entries. 372 else { 373 if (!unfiledEntries.contains(entry)) { 374 unfiledEntries.add(entry); 375 } 376 } 377 } 378 379 // Find the list of groups that the user currently belongs to. 380 List<String> currentGroupNames = new ArrayList<String>(); 381 for (RosterGroup group: getGroups()) { 382 if (group.contains(entry)) { 383 currentGroupNames.add(group.getName()); 384 } 385 } 386 387 // If the packet is not of the type REMOVE then add the entry to the groups 388 if (!RosterPacket.ItemType.remove.equals(item.getItemType())) { 389 // Create the new list of groups the user belongs to. 390 List<String> newGroupNames = new ArrayList<String>(); 391 for (String groupName : item.getGroupNames()) { 392 // Add the group name to the list. 393 newGroupNames.add(groupName); 394 395 // Add the entry to the group. 396 RosterGroup group = getGroup(groupName); 397 if (group == null) { 398 group = createGroup(groupName); 399 groups.put(groupName, group); 400 } 401 // Add the entry. 402 group.addEntryLocal(entry); 403 } 404 405 // We have the list of old and new group names. We now need to 406 // remove the entry from the all the groups it may no longer belong 407 // to. We do this by subracting the new group set from the old. 408 for (String newGroupName : newGroupNames) { 409 currentGroupNames.remove(newGroupName); 410 } 411 } 412 413 // Loop through any groups that remain and remove the entries. 414 // This is neccessary for the case of remote entry removals. 415 for (String groupName : currentGroupNames) { 416 RosterGroup group = getGroup(groupName); 417 group.removeEntryLocal(entry); 418 if (group.getEntryCount() == 0) { 419 groups.remove(groupName); 420 } 421 } 422 // Remove all the groups with no entries. We have to do this because 423 // RosterGroup.removeEntry removes the entry immediately (locally) and the 424 // group could remain empty. 425 // TODO Check the performance/logic for rosters with large number of groups 426 for (RosterGroup group : getGroups()) { 427 if (group.getEntryCount() == 0) { 428 groups.remove(group.getName()); 429 } 430 } 431 } 432 433 /** 434 * Removes a roster entry from the roster. The roster entry will also be removed from the 435 * unfiled entries or from any roster group where it could belong and will no longer be part 436 * of the roster. Note that this is an asynchronous call -- Smack must wait for the server 437 * to send an updated subscription status. 438 * 439 * @param entry a roster entry. 440 * @throws XMPPException if an XMPP error occurs. 441 * @throws IllegalStateException if connection is not logged in or logged in anonymously 442 */ 443 public void removeEntry(RosterEntry entry) throws XMPPException { 444 if (!connection.isAuthenticated()) { 445 throw new IllegalStateException("Not logged in to server."); 446 } 447 if (connection.isAnonymous()) { 448 throw new IllegalStateException("Anonymous users can't have a roster."); 449 } 450 451 // Only remove the entry if it's in the entry list. 452 // The actual removal logic takes place in RosterPacketListenerprocess>>Packet(Packet) 453 if (!entries.containsKey(entry.getUser())) { 454 return; 455 } 456 RosterPacket packet = new RosterPacket(); 457 packet.setType(IQ.Type.SET); 458 RosterPacket.Item item = RosterEntry.toRosterItem(entry); 459 // Set the item type as REMOVE so that the server will delete the entry 460 item.setItemType(RosterPacket.ItemType.remove); 461 packet.addRosterItem(item); 462 PacketCollector collector = connection.createPacketCollector( 463 new PacketIDFilter(packet.getPacketID())); 464 connection.sendPacket(packet); 465 IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout()); 466 collector.cancel(); 467 if (response == null) { 468 throw new XMPPException("No response from the server."); 469 } 470 // If the server replied with an error, throw an exception. 471 else if (response.getType() == IQ.Type.ERROR) { 472 throw new XMPPException(response.getError()); 473 } 474 } 475 476 /** 477 * Returns a count of the entries in the roster. 478 * 479 * @return the number of entries in the roster. 480 */ 481 public int getEntryCount() { 482 return getEntries().size(); 483 } 484 485 /** 486 * Returns an unmodifiable collection of all entries in the roster, including entries 487 * that don't belong to any groups. 488 * 489 * @return all entries in the roster. 490 */ 491 public Collection<RosterEntry> getEntries() { 492 Set<RosterEntry> allEntries = new HashSet<RosterEntry>(); 493 // Loop through all roster groups and add their entries to the answer 494 for (RosterGroup rosterGroup : getGroups()) { 495 allEntries.addAll(rosterGroup.getEntries()); 496 } 497 // Add the roster unfiled entries to the answer 498 allEntries.addAll(unfiledEntries); 499 500 return Collections.unmodifiableCollection(allEntries); 501 } 502 503 /** 504 * Returns a count of the unfiled entries in the roster. An unfiled entry is 505 * an entry that doesn't belong to any groups. 506 * 507 * @return the number of unfiled entries in the roster. 508 */ 509 public int getUnfiledEntryCount() { 510 return unfiledEntries.size(); 511 } 512 513 /** 514 * Returns an unmodifiable collection for the unfiled roster entries. An unfiled entry is 515 * an entry that doesn't belong to any groups. 516 * 517 * @return the unfiled roster entries. 518 */ 519 public Collection<RosterEntry> getUnfiledEntries() { 520 return Collections.unmodifiableList(unfiledEntries); 521 } 522 523 /** 524 * Returns the roster entry associated with the given XMPP address or 525 * <tt>null</tt> if the user is not an entry in the roster. 526 * 527 * @param user the XMPP address of the user (eg "jsmith (at) example.com"). The address could be 528 * in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource"). 529 * @return the roster entry or <tt>null</tt> if it does not exist. 530 */ 531 public RosterEntry getEntry(String user) { 532 if (user == null) { 533 return null; 534 } 535 return entries.get(user.toLowerCase()); 536 } 537 538 /** 539 * Returns true if the specified XMPP address is an entry in the roster. 540 * 541 * @param user the XMPP address of the user (eg "jsmith (at) example.com"). The 542 * address could be in any valid format (e.g. "domain/resource", 543 * "user@domain" or "user@domain/resource"). 544 * @return true if the XMPP address is an entry in the roster. 545 */ 546 public boolean contains(String user) { 547 return getEntry(user) != null; 548 } 549 550 /** 551 * Returns the roster group with the specified name, or <tt>null</tt> if the 552 * group doesn't exist. 553 * 554 * @param name the name of the group. 555 * @return the roster group with the specified name. 556 */ 557 public RosterGroup getGroup(String name) { 558 return groups.get(name); 559 } 560 561 /** 562 * Returns the number of the groups in the roster. 563 * 564 * @return the number of groups in the roster. 565 */ 566 public int getGroupCount() { 567 return groups.size(); 568 } 569 570 /** 571 * Returns an unmodifiable collections of all the roster groups. 572 * 573 * @return an iterator for all roster groups. 574 */ 575 public Collection<RosterGroup> getGroups() { 576 return Collections.unmodifiableCollection(groups.values()); 577 } 578 579 /** 580 * Returns the presence info for a particular user. If the user is offline, or 581 * if no presence data is available (such as when you are not subscribed to the 582 * user's presence updates), unavailable presence will be returned.<p> 583 * <p/> 584 * If the user has several presences (one for each resource), then the presence with 585 * highest priority will be returned. If multiple presences have the same priority, 586 * the one with the "most available" presence mode will be returned. In order, 587 * that's {@link org.jivesoftware.smack.packet.Presence.Mode#chat free to chat}, 588 * {@link org.jivesoftware.smack.packet.Presence.Mode#available available}, 589 * {@link org.jivesoftware.smack.packet.Presence.Mode#away away}, 590 * {@link org.jivesoftware.smack.packet.Presence.Mode#xa extended away}, and 591 * {@link org.jivesoftware.smack.packet.Presence.Mode#dnd do not disturb}.<p> 592 * <p/> 593 * Note that presence information is received asynchronously. So, just after logging 594 * in to the server, presence values for users in the roster may be unavailable 595 * even if they are actually online. In other words, the value returned by this 596 * method should only be treated as a snapshot in time, and may not accurately reflect 597 * other user's presence instant by instant. If you need to track presence over time, 598 * such as when showing a visual representation of the roster, consider using a 599 * {@link RosterListener}. 600 * 601 * @param user an XMPP ID. The address could be in any valid format (e.g. 602 * "domain/resource", "user@domain" or "user@domain/resource"). Any resource 603 * information that's part of the ID will be discarded. 604 * @return the user's current presence, or unavailable presence if the user is offline 605 * or if no presence information is available.. 606 */ 607 public Presence getPresence(String user) { 608 String key = getPresenceMapKey(StringUtils.parseBareAddress(user)); 609 Map<String, Presence> userPresences = presenceMap.get(key); 610 if (userPresences == null) { 611 Presence presence = new Presence(Presence.Type.unavailable); 612 presence.setFrom(user); 613 return presence; 614 } 615 else { 616 // Find the resource with the highest priority 617 // Might be changed to use the resource with the highest availability instead. 618 Presence presence = null; 619 620 for (String resource : userPresences.keySet()) { 621 Presence p = userPresences.get(resource); 622 if (!p.isAvailable()) { 623 continue; 624 } 625 // Chose presence with highest priority first. 626 if (presence == null || p.getPriority() > presence.getPriority()) { 627 presence = p; 628 } 629 // If equal priority, choose "most available" by the mode value. 630 else if (p.getPriority() == presence.getPriority()) { 631 Presence.Mode pMode = p.getMode(); 632 // Default to presence mode of available. 633 if (pMode == null) { 634 pMode = Presence.Mode.available; 635 } 636 Presence.Mode presenceMode = presence.getMode(); 637 // Default to presence mode of available. 638 if (presenceMode == null) { 639 presenceMode = Presence.Mode.available; 640 } 641 if (pMode.compareTo(presenceMode) < 0) { 642 presence = p; 643 } 644 } 645 } 646 if (presence == null) { 647 presence = new Presence(Presence.Type.unavailable); 648 presence.setFrom(user); 649 return presence; 650 } 651 else { 652 return presence; 653 } 654 } 655 } 656 657 /** 658 * Returns the presence info for a particular user's resource, or unavailable presence 659 * if the user is offline or if no presence information is available, such as 660 * when you are not subscribed to the user's presence updates. 661 * 662 * @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource). 663 * @return the user's current presence, or unavailable presence if the user is offline 664 * or if no presence information is available. 665 */ 666 public Presence getPresenceResource(String userWithResource) { 667 String key = getPresenceMapKey(userWithResource); 668 String resource = StringUtils.parseResource(userWithResource); 669 Map<String, Presence> userPresences = presenceMap.get(key); 670 if (userPresences == null) { 671 Presence presence = new Presence(Presence.Type.unavailable); 672 presence.setFrom(userWithResource); 673 return presence; 674 } 675 else { 676 Presence presence = userPresences.get(resource); 677 if (presence == null) { 678 presence = new Presence(Presence.Type.unavailable); 679 presence.setFrom(userWithResource); 680 return presence; 681 } 682 else { 683 return presence; 684 } 685 } 686 } 687 688 /** 689 * Returns an iterator (of Presence objects) for all of a user's current presences 690 * or an unavailable presence if the user is unavailable (offline) or if no presence 691 * information is available, such as when you are not subscribed to the user's presence 692 * updates. 693 * 694 * @param user a XMPP ID, e.g. jdoe (at) example.com. 695 * @return an iterator (of Presence objects) for all the user's current presences, 696 * or an unavailable presence if the user is offline or if no presence information 697 * is available. 698 */ 699 public Iterator<Presence> getPresences(String user) { 700 String key = getPresenceMapKey(user); 701 Map<String, Presence> userPresences = presenceMap.get(key); 702 if (userPresences == null) { 703 Presence presence = new Presence(Presence.Type.unavailable); 704 presence.setFrom(user); 705 return Arrays.asList(presence).iterator(); 706 } 707 else { 708 Collection<Presence> answer = new ArrayList<Presence>(); 709 for (Presence presence : userPresences.values()) { 710 if (presence.isAvailable()) { 711 answer.add(presence); 712 } 713 } 714 if (!answer.isEmpty()) { 715 return answer.iterator(); 716 } 717 else { 718 Presence presence = new Presence(Presence.Type.unavailable); 719 presence.setFrom(user); 720 return Arrays.asList(presence).iterator(); 721 } 722 } 723 } 724 725 /** 726 * Cleans up all resources used by the roster. 727 */ 728 void cleanup() { 729 rosterListeners.clear(); 730 } 731 732 /** 733 * Returns the key to use in the presenceMap for a fully qualified XMPP ID. 734 * The roster can contain any valid address format such us "domain/resource", 735 * "user@domain" or "user@domain/resource". If the roster contains an entry 736 * associated with the fully qualified XMPP ID then use the fully qualified XMPP 737 * ID as the key in presenceMap, otherwise use the bare address. Note: When the 738 * key in presenceMap is a fully qualified XMPP ID, the userPresences is useless 739 * since it will always contain one entry for the user. 740 * 741 * @param user the bare or fully qualified XMPP ID, e.g. jdoe (at) example.com or 742 * jdoe (at) example.com/Work. 743 * @return the key to use in the presenceMap for the fully qualified XMPP ID. 744 */ 745 private String getPresenceMapKey(String user) { 746 if (user == null) { 747 return null; 748 } 749 String key = user; 750 if (!contains(user)) { 751 key = StringUtils.parseBareAddress(user); 752 } 753 return key.toLowerCase(); 754 } 755 756 /** 757 * Changes the presence of available contacts offline by simulating an unavailable 758 * presence sent from the server. After a disconnection, every Presence is set 759 * to offline. 760 */ 761 private void setOfflinePresences() { 762 Presence packetUnavailable; 763 for (String user : presenceMap.keySet()) { 764 Map<String, Presence> resources = presenceMap.get(user); 765 if (resources != null) { 766 for (String resource : resources.keySet()) { 767 packetUnavailable = new Presence(Presence.Type.unavailable); 768 packetUnavailable.setFrom(user + "/" + resource); 769 presencePacketListener.processPacket(packetUnavailable); 770 } 771 } 772 } 773 } 774 775 /** 776 * Fires roster changed event to roster listeners indicating that the 777 * specified collections of contacts have been added, updated or deleted 778 * from the roster. 779 * 780 * @param addedEntries the collection of address of the added contacts. 781 * @param updatedEntries the collection of address of the updated contacts. 782 * @param deletedEntries the collection of address of the deleted contacts. 783 */ 784 private void fireRosterChangedEvent(Collection<String> addedEntries, Collection<String> updatedEntries, 785 Collection<String> deletedEntries) { 786 for (RosterListener listener : rosterListeners) { 787 if (!addedEntries.isEmpty()) { 788 listener.entriesAdded(addedEntries); 789 } 790 if (!updatedEntries.isEmpty()) { 791 listener.entriesUpdated(updatedEntries); 792 } 793 if (!deletedEntries.isEmpty()) { 794 listener.entriesDeleted(deletedEntries); 795 } 796 } 797 } 798 799 /** 800 * Fires roster presence changed event to roster listeners. 801 * 802 * @param presence the presence change. 803 */ 804 private void fireRosterPresenceEvent(Presence presence) { 805 for (RosterListener listener : rosterListeners) { 806 listener.presenceChanged(presence); 807 } 808 } 809 810 /** 811 * An enumeration for the subscription mode options. 812 */ 813 public enum SubscriptionMode { 814 815 /** 816 * Automatically accept all subscription and unsubscription requests. This is 817 * the default mode and is suitable for simple client. More complex client will 818 * likely wish to handle subscription requests manually. 819 */ 820 accept_all, 821 822 /** 823 * Automatically reject all subscription requests. 824 */ 825 reject_all, 826 827 /** 828 * Subscription requests are ignored, which means they must be manually 829 * processed by registering a listener for presence packets and then looking 830 * for any presence requests that have the type Presence.Type.SUBSCRIBE or 831 * Presence.Type.UNSUBSCRIBE. 832 */ 833 manual 834 } 835 836 /** 837 * Listens for all presence packets and processes them. 838 */ 839 private class PresencePacketListener implements PacketListener { 840 841 public void processPacket(Packet packet) { 842 Presence presence = (Presence) packet; 843 String from = presence.getFrom(); 844 String key = getPresenceMapKey(from); 845 846 // If an "available" presence, add it to the presence map. Each presence 847 // map will hold for a particular user a map with the presence 848 // packets saved for each resource. 849 if (presence.getType() == Presence.Type.available) { 850 Map<String, Presence> userPresences; 851 // Get the user presence map 852 if (presenceMap.get(key) == null) { 853 userPresences = new ConcurrentHashMap<String, Presence>(); 854 presenceMap.put(key, userPresences); 855 } 856 else { 857 userPresences = presenceMap.get(key); 858 } 859 // See if an offline presence was being stored in the map. If so, remove 860 // it since we now have an online presence. 861 userPresences.remove(""); 862 // Add the new presence, using the resources as a key. 863 userPresences.put(StringUtils.parseResource(from), presence); 864 // If the user is in the roster, fire an event. 865 RosterEntry entry = entries.get(key); 866 if (entry != null) { 867 fireRosterPresenceEvent(presence); 868 } 869 } 870 // If an "unavailable" packet. 871 else if (presence.getType() == Presence.Type.unavailable) { 872 // If no resource, this is likely an offline presence as part of 873 // a roster presence flood. In that case, we store it. 874 if ("".equals(StringUtils.parseResource(from))) { 875 Map<String, Presence> userPresences; 876 // Get the user presence map 877 if (presenceMap.get(key) == null) { 878 userPresences = new ConcurrentHashMap<String, Presence>(); 879 presenceMap.put(key, userPresences); 880 } 881 else { 882 userPresences = presenceMap.get(key); 883 } 884 userPresences.put("", presence); 885 } 886 // Otherwise, this is a normal offline presence. 887 else if (presenceMap.get(key) != null) { 888 Map<String, Presence> userPresences = presenceMap.get(key); 889 // Store the offline presence, as it may include extra information 890 // such as the user being on vacation. 891 userPresences.put(StringUtils.parseResource(from), presence); 892 } 893 // If the user is in the roster, fire an event. 894 RosterEntry entry = entries.get(key); 895 if (entry != null) { 896 fireRosterPresenceEvent(presence); 897 } 898 } 899 else if (presence.getType() == Presence.Type.subscribe) { 900 if (subscriptionMode == SubscriptionMode.accept_all) { 901 // Accept all subscription requests. 902 Presence response = new Presence(Presence.Type.subscribed); 903 response.setTo(presence.getFrom()); 904 connection.sendPacket(response); 905 } 906 else if (subscriptionMode == SubscriptionMode.reject_all) { 907 // Reject all subscription requests. 908 Presence response = new Presence(Presence.Type.unsubscribed); 909 response.setTo(presence.getFrom()); 910 connection.sendPacket(response); 911 } 912 // Otherwise, in manual mode so ignore. 913 } 914 else if (presence.getType() == Presence.Type.unsubscribe) { 915 if (subscriptionMode != SubscriptionMode.manual) { 916 // Acknowledge and accept unsubscription notification so that the 917 // server will stop sending notifications saying that the contact 918 // has unsubscribed to our presence. 919 Presence response = new Presence(Presence.Type.unsubscribed); 920 response.setTo(presence.getFrom()); 921 connection.sendPacket(response); 922 } 923 // Otherwise, in manual mode so ignore. 924 } 925 // Error presence packets from a bare JID mean we invalidate all existing 926 // presence info for the user. 927 else if (presence.getType() == Presence.Type.error && 928 "".equals(StringUtils.parseResource(from))) 929 { 930 Map<String, Presence> userPresences; 931 if (!presenceMap.containsKey(key)) { 932 userPresences = new ConcurrentHashMap<String, Presence>(); 933 presenceMap.put(key, userPresences); 934 } 935 else { 936 userPresences = presenceMap.get(key); 937 // Any other presence data is invalidated by the error packet. 938 userPresences.clear(); 939 } 940 // Set the new presence using the empty resource as a key. 941 userPresences.put("", presence); 942 // If the user is in the roster, fire an event. 943 RosterEntry entry = entries.get(key); 944 if (entry != null) { 945 fireRosterPresenceEvent(presence); 946 } 947 } 948 } 949 } 950 951 /** 952 * Listen for empty IQ results which indicate that the client has already a current 953 * roster version 954 * @author Till Klocke 955 * 956 */ 957 958 private class RosterResultListener implements PacketListener{ 959 960 public void processPacket(Packet packet) { 961 if(packet instanceof IQ){ 962 IQ result = (IQ)packet; 963 if(result.getType().equals(IQ.Type.RESULT) && result.getExtensions().isEmpty()){ 964 Collection<String> addedEntries = new ArrayList<String>(); 965 Collection<String> updatedEntries = new ArrayList<String>(); 966 Collection<String> deletedEntries = new ArrayList<String>(); 967 if(persistentStorage!=null){ 968 for(RosterPacket.Item item : persistentStorage.getEntries()){ 969 insertRosterItem(item,addedEntries,updatedEntries,deletedEntries); 970 } 971 } 972 synchronized (Roster.this) { 973 rosterInitialized = true; 974 Roster.this.notifyAll(); 975 } 976 fireRosterChangedEvent(addedEntries,updatedEntries,deletedEntries); 977 } 978 } 979 connection.removePacketListener(this); 980 } 981 } 982 983 /** 984 * Listens for all roster packets and processes them. 985 */ 986 private class RosterPacketListener implements PacketListener { 987 988 public void processPacket(Packet packet) { 989 // Keep a registry of the entries that were added, deleted or updated. An event 990 // will be fired for each affected entry 991 Collection<String> addedEntries = new ArrayList<String>(); 992 Collection<String> updatedEntries = new ArrayList<String>(); 993 Collection<String> deletedEntries = new ArrayList<String>(); 994 995 String version=null; 996 RosterPacket rosterPacket = (RosterPacket) packet; 997 List<RosterPacket.Item> rosterItems = new ArrayList<RosterPacket.Item>(); 998 for(RosterPacket.Item item : rosterPacket.getRosterItems()){ 999 rosterItems.add(item); 1000 } 1001 //Here we check if the server send a versioned roster, if not we do not use 1002 //the roster storage to store entries and work like in the old times 1003 if(rosterPacket.getVersion()==null){ 1004 persistentStorage=null; 1005 } else{ 1006 version = rosterPacket.getVersion(); 1007 } 1008 1009 if(persistentStorage!=null && !rosterInitialized){ 1010 for(RosterPacket.Item item : persistentStorage.getEntries()){ 1011 rosterItems.add(item); 1012 } 1013 } 1014 1015 for (RosterPacket.Item item : rosterItems) { 1016 insertRosterItem(item,addedEntries,updatedEntries,deletedEntries); 1017 } 1018 if(persistentStorage!=null){ 1019 for (RosterPacket.Item i : rosterPacket.getRosterItems()){ 1020 if(i.getItemType().equals(RosterPacket.ItemType.remove)){ 1021 persistentStorage.removeEntry(i.getUser()); 1022 } 1023 else{ 1024 persistentStorage.addEntry(i, version); 1025 } 1026 } 1027 } 1028 // Mark the roster as initialized. 1029 synchronized (Roster.this) { 1030 rosterInitialized = true; 1031 Roster.this.notifyAll(); 1032 } 1033 1034 // Fire event for roster listeners. 1035 fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries); 1036 } 1037 } 1038 } 1039