Shuyi Chen | d7955ce | 2013-05-22 14:51:55 -0700 | [diff] [blame] | 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@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@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@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@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@example.com or |
| 742 | * jdoe@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 | } |