1 /* 2 * Copyright (C) 2005-2008 Jive Software. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.jivesoftware.openfire.roster; 18 19 import org.jivesoftware.database.JiveID; 20 import org.jivesoftware.openfire.*; 21 import org.jivesoftware.openfire.group.Group; 22 import org.jivesoftware.openfire.group.GroupManager; 23 import org.jivesoftware.openfire.privacy.PrivacyList; 24 import org.jivesoftware.openfire.privacy.PrivacyListManager; 25 import org.jivesoftware.openfire.session.ClientSession; 26 import org.jivesoftware.openfire.user.UserAlreadyExistsException; 27 import org.jivesoftware.openfire.user.UserNameManager; 28 import org.jivesoftware.openfire.user.UserNotFoundException; 29 import org.jivesoftware.util.JiveConstants; 30 import org.jivesoftware.util.cache.CacheSizes; 31 import org.jivesoftware.util.cache.Cacheable; 32 import org.jivesoftware.util.cache.CannotCalculateSizeException; 33 import org.jivesoftware.util.cache.ExternalizableUtil; 34 import org.slf4j.Logger; 35 import org.slf4j.LoggerFactory; 36 import org.xmpp.packet.IQ; 37 import org.xmpp.packet.JID; 38 import org.xmpp.packet.Presence; 39 40 import java.io.Externalizable; 41 import java.io.IOException; 42 import java.io.ObjectInput; 43 import java.io.ObjectOutput; 44 import java.util.*; 45 import java.util.concurrent.ConcurrentHashMap; 46 import java.util.concurrent.ConcurrentMap; 47 48 /** 49 * <p>A roster is a list of users that the user wishes to know if they are online.</p> 50 * <p>Rosters are similar to buddy groups in popular IM clients. The Roster class is 51 * a representation of the roster data.</p> 52 * 53 * <p>Updates to this roster is effectively a change to the user's roster. To reflect this, 54 * the changes to this class will automatically update the persistently stored roster, as well as 55 * send out update announcements to all logged in user sessions.</p> 56 * 57 * @author Gaston Dombiak 58 */ 59 @JiveID(JiveConstants.ROSTER) 60 public class Roster implements Cacheable, Externalizable { 61 62 private static final Logger Log = LoggerFactory.getLogger(Roster.class); 63 64 /** 65 * Roster item cache - table: key jabberid string; value roster item. 66 */ 67 protected ConcurrentMap<String, RosterItem> rosterItems = new ConcurrentHashMap<>(); 68 /** 69 * Contacts with subscription FROM that only exist due to shared groups 70 * key: jabberid string; value: groups why the implicit roster item exists (aka invisibleSharedGroups). 71 */ 72 protected ConcurrentMap<String, Set<String>> implicitFrom = new ConcurrentHashMap<>(); 73 74 private String username; 75 76 /** 77 * Constructor added for Externalizable. Do not use this constructor. 78 */ Roster()79 public Roster() { 80 } 81 82 /** 83 * Create a roster for the given user, pulling the existing roster items 84 * out of the backend storage provider. The roster will also include items that 85 * belong to the user's shared groups.<p> 86 * 87 * RosterItems that ONLY belong to shared groups won't be persistent unless the user 88 * explicitly subscribes to the contact's presence, renames the contact in his roster or adds 89 * the item to a personal group.<p> 90 * 91 * This constructor is not public and instead you should use 92 * {@link org.jivesoftware.openfire.roster.RosterManager#getRoster(String)}. 93 * 94 * @param username The username of the user that owns this roster 95 */ Roster(String username)96 Roster(String username) { 97 final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager(); 98 99 this.username = username; 100 101 // Get the shared groups of this user 102 Collection<Group> sharedGroups = rosterManager.getSharedGroups(username); 103 //Collection<Group> userGroups = GroupManager.getInstance().getGroups(getUserJID()); 104 105 // Add RosterItems that belong to the personal roster 106 Iterator<RosterItem> items = RosterManager.getRosterItemProvider().getItems(username); 107 while (items.hasNext()) { 108 RosterItem item = items.next(); 109 // Check if the item (i.e. contact) belongs to a shared group of the user. Add the 110 // shared group (if any) to this item 111 for (Group group : sharedGroups) { 112 if (group.isUser(item.getJid())) { 113 // TODO Group name conflicts are not being considered (do we need this?) 114 item.addSharedGroup(group); 115 item.setSubStatus(RosterItem.SUB_BOTH); 116 } 117 } 118 rosterItems.put(item.getJid().toBareJID(), item); 119 } 120 // Add RosterItems that belong only to shared groups 121 Map<JID, List<Group>> sharedUsers = getSharedUsers(sharedGroups); 122 for (Map.Entry<JID, List<Group>> entry : sharedUsers.entrySet()) { 123 JID jid = entry.getKey(); 124 List<Group> groups = entry.getValue(); 125 try { 126 Collection<Group> itemGroups = new ArrayList<>(); 127 String nickname = ""; 128 RosterItem item = new RosterItem(jid, RosterItem.SUB_TO, RosterItem.ASK_NONE, 129 RosterItem.RECV_NONE, nickname, null); 130 // Add the shared groups to the new roster item 131 for (Group group : groups) { 132 if (group.isUser(jid)) { 133 item.addSharedGroup(group); 134 itemGroups.add(group); 135 } else { 136 item.addInvisibleSharedGroup(group); 137 } 138 } 139 // Set subscription type to BOTH if the roster user belongs to a shared group 140 // that is mutually visible with a shared group of the new roster item 141 if (rosterManager.hasMutualVisibility(username, sharedGroups, jid, itemGroups)) { 142 item.setSubStatus(RosterItem.SUB_BOTH); 143 } else { 144 // Set subscription type to FROM if the contact does not belong to any of 145 // the associated shared groups 146 boolean belongsToGroup = false; 147 for (Group group : groups) { 148 if (group.isUser(jid)) { 149 belongsToGroup = true; 150 } 151 } 152 if (!belongsToGroup) { 153 item.setSubStatus(RosterItem.SUB_FROM); 154 } 155 } 156 // Set nickname and store in memory only if subscription type is not FROM. 157 // Roster items with subscription type FROM that exist only because of shared 158 // groups will be recreated on demand in #getRosterItem(JID) and #isRosterItem() 159 // but will never be stored in memory nor in the database. This is an important 160 // optimization to reduce objects in memory and avoid loading users in memory 161 // to get their nicknames that will never be shown 162 if (item.getSubStatus() != RosterItem.SUB_FROM) { 163 item.setNickname(UserNameManager.getUserName(jid)); 164 rosterItems.put(item.getJid().toBareJID(), item); 165 } else { 166 // Cache information about shared contacts with subscription status FROM 167 implicitFrom 168 .put(item.getJid().toBareJID(), item.getInvisibleSharedGroupsNames()); 169 } 170 } catch (UserNotFoundException e) { 171 Log.error("Groups (" + groups + ") include non-existent username (" + 172 jid.getNode() + 173 ")"); 174 } 175 } 176 // Fire event indicating that a roster has just been loaded 177 RosterEventDispatcher.rosterLoaded(this); 178 } 179 180 /** 181 * Returns true if the specified user is a member of the roster, false otherwise. 182 * 183 * @param user the user object to check. 184 * @return true if the specified user is a member of the roster, false otherwise. 185 */ isRosterItem(JID user)186 public boolean isRosterItem(JID user) { 187 // Optimization: Check if the contact has a FROM subscription due to shared groups 188 // (only when not present in the rosterItems collection) 189 return rosterItems.containsKey(user.toBareJID()) || getImplicitRosterItem(user) != null; 190 } 191 192 /** 193 * Returns a collection of users in this roster.<p> 194 * 195 * Note: Roster items with subscription type FROM that exist only because of shared groups 196 * are not going to be returned. 197 * 198 * @return a collection of users in this roster. 199 */ getRosterItems()200 public Collection<RosterItem> getRosterItems() { 201 return Collections.unmodifiableCollection(rosterItems.values()); 202 } 203 204 /** 205 * Returns the roster item that is associated with the specified JID. If no roster item 206 * was found then a UserNotFoundException will be thrown. 207 * 208 * @param user the XMPPAddress for the roster item to retrieve 209 * @return The roster item associated with the user XMPPAddress. 210 * @throws UserNotFoundException if no roster item was found for the specified JID. 211 */ getRosterItem(JID user)212 public RosterItem getRosterItem(JID user) throws UserNotFoundException { 213 RosterItem item = rosterItems.get(user.toBareJID()); 214 if (item == null) { 215 // Optimization: Check if the contact has a FROM subscription due to shared groups 216 item = getImplicitRosterItem(user); 217 if (item == null) { 218 throw new UserNotFoundException(user.toBareJID()); 219 } 220 } 221 return item; 222 } 223 224 /** 225 * Returns a roster item if the specified user has a subscription of type FROM to this 226 * user and the susbcription only exists due to some shared groups or otherwise 227 * {@code null}. This method assumes that this user does not have a subscription to 228 * the contact. In other words, this method will not check if there should be a subscription 229 * of type TO ot BOTH. 230 * 231 * @param user the contact to check if he is subscribed to the presence of this user. 232 * @return a roster item if the specified user has a subscription of type FROM to this 233 * user and the susbcription only exists due to some shared groups or otherwise null. 234 */ getImplicitRosterItem(JID user)235 private RosterItem getImplicitRosterItem(JID user) { 236 Set<String> invisibleSharedGroups = implicitFrom.get(user.toBareJID()); 237 if (invisibleSharedGroups != null) { 238 RosterItem rosterItem = new RosterItem(user, RosterItem.SUB_FROM, RosterItem.ASK_NONE, 239 RosterItem.RECV_NONE, "", null); 240 rosterItem.setInvisibleSharedGroupsNames(invisibleSharedGroups); 241 return rosterItem; 242 } 243 return null; 244 } 245 246 /** 247 * Create a new item to the roster. Roster items may not be created that contain the same user 248 * address as an existing item. 249 * 250 * @param user The item to add to the roster. 251 * @param push True if the new item must be pushed to the user. 252 * @param persistent True if the new roster item should be persisted to the DB. 253 * @return the roster item 254 * @throws UserAlreadyExistsException if the user is already in the roster 255 * @throws SharedGroupException if the group is a shared group 256 */ createRosterItem(JID user, boolean push, boolean persistent)257 public RosterItem createRosterItem(JID user, boolean push, boolean persistent) 258 throws UserAlreadyExistsException, SharedGroupException { 259 return createRosterItem(user, null, null, push, persistent); 260 } 261 262 /** 263 * Create a new item to the roster. Roster items may not be created that contain the same user 264 * address as an existing item. 265 * 266 * @param user The item to add to the roster. 267 * @param nickname The nickname for the roster entry (can be null). 268 * @param push True if the new item must be push to the user. 269 * @param persistent True if the new roster item should be persisted to the DB. 270 * @param groups The list of groups to assign this roster item to (can be null) 271 * @return the roster item 272 * @throws UserAlreadyExistsException if the user is already in the roster 273 * @throws SharedGroupException if the group is a shared group 274 */ createRosterItem(JID user, String nickname, List<String> groups, boolean push, boolean persistent)275 public RosterItem createRosterItem(JID user, String nickname, List<String> groups, boolean push, 276 boolean persistent) 277 throws UserAlreadyExistsException, SharedGroupException { 278 return provideRosterItem(user, nickname, groups, push, persistent); 279 } 280 281 /** 282 * Create a new item to the roster based as a copy of the given item. 283 * Roster items may not be created that contain the same user address 284 * as an existing item in the roster. 285 * 286 * @param item the item to copy and add to the roster. 287 * @throws UserAlreadyExistsException if the user is already in the roster 288 * @throws SharedGroupException if the group is a shared group 289 */ createRosterItem(org.xmpp.packet.Roster.Item item)290 public void createRosterItem(org.xmpp.packet.Roster.Item item) 291 throws UserAlreadyExistsException, SharedGroupException { 292 provideRosterItem(item.getJID(), item.getName(), new ArrayList<>(item.getGroups()), true, true); 293 } 294 295 /** 296 * Generate a new RosterItem for use with createRosterItem. 297 * 298 * @param user The roster jid address to create the roster item for. 299 * @param nickname The nickname to assign the item (or null for none). 300 * @param groups The groups the item belongs to (or null for none). 301 * @param push True if the new item must be push to the user. 302 * @param persistent True if the new roster item should be persisted to the DB. 303 * @return The newly created roster items ready to be stored by the Roster item's hash table 304 * @throws UserAlreadyExistsException if the user is already in the roster 305 * @throws SharedGroupException if the group is a shared group 306 */ provideRosterItem(JID user, String nickname, List<String> groups, boolean push, boolean persistent)307 protected RosterItem provideRosterItem(JID user, String nickname, List<String> groups, 308 boolean push, boolean persistent) 309 throws UserAlreadyExistsException, SharedGroupException { 310 if (groups != null && !groups.isEmpty()) { 311 // Raise an error if the groups the item belongs to include a shared group 312 for (String groupDisplayName : groups) { 313 Collection<Group> groupsWithProp = GroupManager 314 .getInstance() 315 .search("sharedRoster.displayName", groupDisplayName); 316 for ( Group group : groupsWithProp ) 317 { 318 String showInRoster = group.getProperties().get( "sharedRoster.showInRoster" ); 319 if ( showInRoster != null && !showInRoster.equals( "nobody" ) ) 320 { 321 throw new SharedGroupException( "Cannot add an item to a shared group" ); 322 } 323 } 324 } 325 } 326 org.xmpp.packet.Roster roster = new org.xmpp.packet.Roster(); 327 roster.setType(IQ.Type.set); 328 org.xmpp.packet.Roster.Item item = roster.addItem(user, nickname, null, 329 org.xmpp.packet.Roster.Subscription.none, groups); 330 331 RosterItem rosterItem = new RosterItem(item); 332 // Fire event indicating that a roster item is about to be added 333 persistent = RosterEventDispatcher.addingContact(this, rosterItem, persistent); 334 335 // Check if we need to make the new roster item persistent 336 if (persistent) { 337 rosterItem = RosterManager.getRosterItemProvider().createItem(username, rosterItem); 338 } 339 340 if (push) { 341 // Broadcast the roster push to the user 342 broadcast(roster); 343 } 344 345 rosterItems.put(user.toBareJID(), rosterItem); 346 347 // Fire event indicating that a roster item has been added 348 RosterEventDispatcher.contactAdded(this, rosterItem); 349 350 return rosterItem; 351 } 352 353 /** 354 * Update an item that is already in the roster. 355 * 356 * @param item the item to update in the roster. 357 * @throws UserNotFoundException If the roster item for the given user doesn't already exist 358 */ updateRosterItem(RosterItem item)359 public void updateRosterItem(RosterItem item) throws UserNotFoundException { 360 // Check if we need to convert an implicit roster item into an explicit one 361 if (implicitFrom.remove(item.getJid().toBareJID()) != null) { 362 // Ensure that the item is an explicit roster item 363 rosterItems.put(item.getJid().toBareJID(), item); 364 // Fire event indicating that a roster item has been updated 365 RosterEventDispatcher.contactUpdated(this, item); 366 } 367 if (rosterItems.putIfAbsent(item.getJid().toBareJID(), item) == null) { 368 rosterItems.remove(item.getJid().toBareJID()); 369 if (item.getSubStatus() != RosterItem.SUB_NONE) { 370 throw new UserNotFoundException(item.getJid().toBareJID()); 371 } 372 return; 373 } 374 // Check if the item is not persistent 375 if (item.getID() == 0) { 376 // Make the item persistent if a new nickname has been set for a shared contact 377 if (item.isShared()) { 378 // Do nothing if item is only shared and it is using the default user name 379 if (item.isOnlyShared()) { 380 String defaultContactName; 381 try { 382 defaultContactName = UserNameManager.getUserName(item.getJid()); 383 } catch (UserNotFoundException e) { 384 // Cannot update a roster item for a local user that does not exist 385 defaultContactName = item.getNickname(); 386 } 387 if (defaultContactName.equals(item.getNickname())) { 388 return; 389 } 390 } 391 try { 392 RosterManager.getRosterItemProvider().createItem(username, item); 393 } catch (UserAlreadyExistsException e) { 394 // Do nothing. We shouldn't be here. 395 Log.warn( "Unexpected error while updating roster item for user '{}'!", username, e); 396 } 397 } else { 398 // Item is not persistent and it does not belong to a shared contact so do nothing 399 } 400 } else { 401 // Update the backend data store 402 RosterManager.getRosterItemProvider().updateItem(username, item); 403 } 404 // broadcast roster update 405 // Do not push items with a state of "None + Pending In" 406 if (item.getSubStatus() != RosterItem.SUB_NONE || 407 item.getRecvStatus() != RosterItem.RECV_SUBSCRIBE && !isSubscriptionRejected(item)) { 408 broadcast(item, true); 409 } 410 /*if (item.getSubStatus() == RosterItem.SUB_BOTH || item.getSubStatus() == RosterItem.SUB_TO) { 411 probePresence(item.getJid()); 412 }*/ 413 // Fire event indicating that a roster item has been updated 414 RosterEventDispatcher.contactUpdated(this, item); 415 } 416 417 /** 418 * Returns true if roster item represents a rejected subscription request. 419 * 420 * @param item The roster item. 421 * @return True, if the roster item represents a rejected subscription request. 422 */ isSubscriptionRejected(RosterItem item)423 private static boolean isSubscriptionRejected(RosterItem item) { 424 return item.getSubStatus() == RosterItem.SUB_NONE && 425 item.getRecvStatus() == RosterItem.RECV_NONE && 426 item.getAskStatus() == RosterItem.AskType.NONE; 427 } 428 429 /** 430 * Remove a user from the roster. 431 * 432 * @param user the user to remove from the roster. 433 * @param doChecking flag that indicates if checkings should be done before deleting the user. 434 * @return The roster item being removed or null if none existed 435 * @throws SharedGroupException if the user to remove belongs to a shared group 436 */ deleteRosterItem(JID user, boolean doChecking)437 public RosterItem deleteRosterItem(JID user, boolean doChecking) throws SharedGroupException { 438 // Answer an error if user (i.e. contact) to delete belongs to a shared group 439 RosterItem itemToRemove = rosterItems.get(user.toBareJID()); 440 if (doChecking && itemToRemove != null && !itemToRemove.getSharedGroups().isEmpty()) { 441 throw new SharedGroupException("Cannot remove contact that belongs to a shared group"); 442 } 443 444 if (itemToRemove != null) { 445 RosterItem.SubType subType = itemToRemove.getSubStatus(); 446 447 // Cancel any existing presence subscription between the user and the contact 448 if (subType == RosterItem.SUB_TO || subType == RosterItem.SUB_BOTH) { 449 Presence presence = new Presence(); 450 presence.setFrom(XMPPServer.getInstance().createJID(username, null)); 451 presence.setTo(itemToRemove.getJid()); 452 presence.setType(Presence.Type.unsubscribe); 453 XMPPServer.getInstance().getPacketRouter().route(presence); 454 } 455 456 // cancel any existing presence subscription between the contact and the user 457 if (subType == RosterItem.SUB_FROM || subType == RosterItem.SUB_BOTH) { 458 Presence presence = new Presence(); 459 presence.setFrom(XMPPServer.getInstance().createJID(username, null)); 460 presence.setTo(itemToRemove.getJid()); 461 presence.setType(Presence.Type.unsubscribed); 462 XMPPServer.getInstance().getPacketRouter().route(presence); 463 } 464 465 // If removing the user was successful, remove the user from the subscriber list: 466 RosterItem item = rosterItems.remove(user.toBareJID()); 467 468 if (item != null) { 469 // Delete the item from the provider if the item is persistent. RosteItems that only 470 // belong to shared groups won't be persistent 471 if (item.getID() > 0) { 472 // If removing the user was successful, remove the user from the backend store 473 RosterManager.getRosterItemProvider().deleteItem(username, item.getID()); 474 } 475 476 // Broadcast the update to the user 477 org.xmpp.packet.Roster roster = new org.xmpp.packet.Roster(); 478 roster.setType(IQ.Type.set); 479 roster.addItem(user, org.xmpp.packet.Roster.Subscription.remove); 480 broadcast(roster); 481 // Fire event indicating that a roster item has been deleted 482 RosterEventDispatcher.contactDeleted(this, item); 483 } 484 485 return item; 486 } else { 487 // Verify if the item being removed is an implicit roster item 488 // that only exists due to some shared group 489 RosterItem item = getImplicitRosterItem(user); 490 if (item != null) { 491 implicitFrom.remove(user.toBareJID()); 492 // If the contact being removed is not a local user then ACK unsubscription 493 if (!XMPPServer.getInstance().isLocal(user)) { 494 Presence presence = new Presence(); 495 presence.setFrom(XMPPServer.getInstance().createJID(username, null)); 496 presence.setTo(user); 497 presence.setType(Presence.Type.unsubscribed); 498 XMPPServer.getInstance().getPacketRouter().route(presence); 499 } 500 // Fire event indicating that a roster item has been deleted 501 RosterEventDispatcher.contactDeleted(this, item); 502 } 503 } 504 505 return null; 506 } 507 508 /** 509 * <p>Return the username of the user or chatbot that owns this roster.</p> 510 * 511 * @return the username of the user or chatbot that owns this roster 512 */ getUsername()513 public String getUsername() { 514 return username; 515 } 516 517 /** 518 * <p>Obtain a 'roster reset', a snapshot of the full cached roster as an Roster.</p> 519 * 520 * @return The roster reset (snapshot) as an Roster 521 */ getReset()522 public org.xmpp.packet.Roster getReset() { 523 org.xmpp.packet.Roster roster = new org.xmpp.packet.Roster(); 524 525 // Add the roster items (includes the personal roster and shared groups) to the answer 526 for (RosterItem item : rosterItems.values()) { 527 // Do not include items with status FROM that exist only because of shared groups 528 if (item.isOnlyShared() && item.getSubStatus() == RosterItem.SUB_FROM) { 529 continue; 530 } 531 org.xmpp.packet.Roster.Ask ask = getAskStatus(item.getAskStatus()); 532 org.xmpp.packet.Roster.Subscription sub = org.xmpp.packet.Roster.Subscription.valueOf(item.getSubStatus() 533 .getName()); 534 // Set the groups to broadcast (include personal and shared groups) 535 List<String> groups = new ArrayList<>(item.getGroups()); 536 if (groups.contains(null)) { 537 Log.warn("A group is null in roster item: " + item.getJid() + " of user: " + 538 getUsername()); 539 } 540 for (Group sharedGroup : item.getSharedGroups()) { 541 String displayName = sharedGroup.getProperties().get("sharedRoster.displayName"); 542 if (displayName != null) { 543 groups.add(displayName); 544 } else { 545 // Do not add the shared group if it does not have a displayName. 546 Log.warn("Found shared group: " + sharedGroup.getName() + 547 " with no displayName"); 548 } 549 } 550 // Do not push items with a state of "None + Pending In" 551 if (item.getSubStatus() != RosterItem.SUB_NONE || 552 item.getRecvStatus() != RosterItem.RECV_SUBSCRIBE && !isSubscriptionRejected(item)) { 553 roster.addItem(item.getJid(), item.getNickname(), ask, sub, groups); 554 } 555 } 556 return roster; 557 } 558 getAskStatus(RosterItem.AskType askType)559 private org.xmpp.packet.Roster.Ask getAskStatus(RosterItem.AskType askType) { 560 if (askType == null || askType == RosterItem.AskType.NONE) { 561 return null; 562 } 563 return org.xmpp.packet.Roster.Ask.valueOf(askType.name().toLowerCase()); 564 } 565 566 /** 567 * <p>Broadcast the presence update to all subscribers of the roster.</p> 568 * <p>Any presence change typically results in a broadcast to the roster members.</p> 569 * 570 * @param packet The presence packet to broadcast 571 */ broadcastPresence(Presence packet)572 public void broadcastPresence(Presence packet) { 573 final RoutingTable routingTable = XMPPServer.getInstance().getRoutingTable(); 574 575 if (routingTable == null) { 576 return; 577 } 578 // Get the privacy list of this user 579 PrivacyList list = null; 580 JID from = packet.getFrom(); 581 if (from != null) { 582 // Try to use the active list of the session. If none was found then try to use 583 // the default privacy list of the session 584 ClientSession session = SessionManager.getInstance().getSession(from); 585 if (session != null) { 586 list = session.getActiveList(); 587 list = list == null ? session.getDefaultList() : list; 588 } 589 } 590 if (list == null) { 591 // No privacy list was found (based on the session) so check if there is a default list 592 list = PrivacyListManager.getInstance().getDefaultPrivacyList(username); 593 } 594 // Broadcast presence to subscribed entities 595 for (RosterItem item : rosterItems.values()) { 596 if (item.getSubStatus() == RosterItem.SUB_BOTH || item.getSubStatus() == RosterItem.SUB_FROM) { 597 packet.setTo(item.getJid()); 598 if (list != null && list.shouldBlockPacket(packet)) { 599 // Outgoing presence notifications are blocked for this contact 600 continue; 601 } 602 JID searchNode = new JID(item.getJid().getNode(), item.getJid().getDomain(), null, true); 603 final List<JID> routingTableRoutes = routingTable.getRoutes(searchNode, null); 604 for (JID jid : routingTableRoutes) { 605 try { 606 routingTable.routePacket(jid, packet, false); 607 } catch (Exception e) { 608 // Theoretically only happens if session has been closed. 609 Log.debug(e.getMessage(), e); 610 } 611 } 612 } 613 } 614 // Broadcast presence to shared contacts whose subscription status is FROM 615 final Set<String> implicitFroms = implicitFrom.keySet(); 616 for (String contact : implicitFroms) { 617 if (contact.contains("@")) { 618 String node = contact.substring(0, contact.lastIndexOf("@")); 619 String domain = contact.substring(contact.lastIndexOf("@") + 1); 620 node = JID.escapeNode(node); 621 contact = new JID(node, domain, null).toBareJID(); 622 } 623 624 packet.setTo(contact); 625 if (list != null && list.shouldBlockPacket(packet)) { 626 // Outgoing presence notifications are blocked for this contact 627 continue; 628 } 629 630 final List<JID> routingTableRoutes = routingTable.getRoutes(new JID(contact), null); 631 for (JID jid : routingTableRoutes) { 632 try { 633 routingTable.routePacket(jid, packet, false); 634 } catch (Exception e) { 635 // Theoretically only happens if session has been closed. 636 Log.debug(e.getMessage(), e); 637 } 638 } 639 } 640 if (from != null) { 641 // Broadcast presence to all resources of the user. 642 SessionManager.getInstance().broadcastPresenceToResources( from, packet); 643 } 644 } 645 646 /** 647 * Returns the list of users that belong ONLY to a shared group of this user. If the contact 648 * belongs to the personal roster and a shared group then it wont' be included in the answer. 649 * 650 * @param sharedGroups the shared groups of this user. 651 * @return the list of users that belong ONLY to a shared group of this user. 652 */ getSharedUsers(Collection<Group> sharedGroups)653 private Map<JID, List<Group>> getSharedUsers(Collection<Group> sharedGroups) { 654 final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager(); 655 // Get the users to process from the shared groups. Users that belong to different groups 656 // will have one entry in the map associated with all the groups 657 Map<JID, List<Group>> sharedGroupUsers = new HashMap<>(); 658 for (Group group : sharedGroups) { 659 // Get all the users that should be in this roster 660 Collection<JID> users = rosterManager.getSharedUsersForRoster(group, this); 661 // Add the users of the group to the general list of users to process 662 JID userJID = getUserJID(); 663 for (JID jid : users) { 664 // Add the user to the answer if the user doesn't belong to the personal roster 665 // (since we have already added the user to the answer) 666 boolean isRosterItem = rosterItems.containsKey(jid.toBareJID()); 667 if (!isRosterItem && !userJID.equals(jid)) { 668 List<Group> groups = sharedGroupUsers.get(jid); 669 if (groups == null) { 670 groups = new ArrayList<>(); 671 sharedGroupUsers.put(jid, groups); 672 } 673 groups.add(group); 674 } 675 } 676 } 677 return sharedGroupUsers; 678 } 679 broadcast(org.xmpp.packet.Roster roster)680 private void broadcast(org.xmpp.packet.Roster roster) { 681 JID recipient = XMPPServer.getInstance().createJID(username, null, true); 682 roster.setTo(recipient); 683 684 // When roster versioning is enabled, the server MUST include 685 // the updated roster version with each roster push. 686 if (RosterManager.isRosterVersioningEnabled()) { 687 roster.getChildElement().addAttribute("ver", String.valueOf( roster.hashCode() ) ); 688 } 689 SessionManager.getInstance().userBroadcast(username, roster); 690 } 691 692 /** 693 * Broadcasts the RosterItem to all the connected resources of this user. Due to performance 694 * optimizations and due to some clients errors that are showing items with subscription status 695 * FROM we added a flag that indicates if a roster items that exists only because of a shared 696 * group with subscription status FROM will not be sent. 697 * 698 * @param item the item to broadcast. 699 * @param optimize true indicates that items that exists only because of a shared 700 * group with subscription status FROM will not be sent 701 */ broadcast(RosterItem item, boolean optimize)702 public void broadcast(RosterItem item, boolean optimize) { 703 // Do not broadcast items with status FROM that exist only because of shared groups 704 if (optimize && item.isOnlyShared() && item.getSubStatus() == RosterItem.SUB_FROM) { 705 return; 706 } 707 // Set the groups to broadcast (include personal and shared groups) 708 List<String> groups = new ArrayList<>(item.getGroups()); 709 for (Group sharedGroup : item.getSharedGroups()) { 710 String displayName = sharedGroup.getProperties().get("sharedRoster.displayName"); 711 if (displayName != null) { 712 groups.add(displayName); 713 } 714 } 715 716 org.xmpp.packet.Roster roster = new org.xmpp.packet.Roster(); 717 roster.setType(IQ.Type.set); 718 roster.addItem(item.getJid(), item.getNickname(), 719 getAskStatus(item.getAskStatus()), 720 org.xmpp.packet.Roster.Subscription.valueOf(item.getSubStatus().getName()), 721 groups); 722 broadcast(roster); 723 } 724 725 /** 726 * Sends a presence probe to the probee for each connected resource of this user. 727 */ probePresence(JID probee)728 private void probePresence(JID probee) { 729 final PresenceManager presenceManager = XMPPServer.getInstance().getPresenceManager(); 730 for (ClientSession session : SessionManager.getInstance().getSessions(username)) { 731 presenceManager.probePresence(session.getAddress(), probee); 732 } 733 } 734 735 @Override getCachedSize()736 public int getCachedSize() throws CannotCalculateSizeException { 737 // Approximate the size of the object in bytes by calculating the size 738 // of the content of each field, if that content is likely to be eligable for 739 // garbage collection if the Roster instance is dereferenced. 740 int size = 0; 741 size += CacheSizes.sizeOfObject(); // overhead of object 742 size += CacheSizes.sizeOfCollection(rosterItems.values()); // roster item cache 743 size += CacheSizes.sizeOfString(username); // username 744 745 // implicitFrom 746 for (Map.Entry<String, Set<String>> entry : implicitFrom.entrySet()) { 747 size += CacheSizes.sizeOfString(entry.getKey()); 748 size += CacheSizes.sizeOfCollection(entry.getValue()); 749 } 750 751 return size; 752 } 753 754 /** 755 * Update the roster since a group user has been added to a shared group. Create a new 756 * RosterItem if the there doesn't exist an item for the added user. The new RosterItem won't be 757 * saved to the backend store unless the user explicitly subscribes to the contact's presence, 758 * renames the contact in his roster or adds the item to a personal group. Otherwise the shared 759 * group will be added to the shared groups lists. In any case an update broadcast will be sent 760 * to all the users logged resources. 761 * 762 * @param group the shared group where the user was added. 763 * @param addedUser the contact to update in the roster. 764 */ addSharedUser(Group group, JID addedUser)765 void addSharedUser(Group group, JID addedUser) { 766 boolean newItem; 767 RosterItem item; 768 try { 769 // Get the RosterItem for the *local* user to add 770 item = getRosterItem(addedUser); 771 // Do nothing if the item already includes the shared group 772 if (item.getSharedGroups().contains(group)) { 773 return; 774 } 775 newItem = false; 776 } catch (UserNotFoundException e) { 777 try { 778 // Create a new RosterItem for this new user 779 String nickname = UserNameManager.getUserName(addedUser); 780 item = 781 new RosterItem(addedUser, RosterItem.SUB_BOTH, RosterItem.ASK_NONE, 782 RosterItem.RECV_NONE, nickname, null); 783 // Add the new item to the list of items 784 rosterItems.put(item.getJid().toBareJID(), item); 785 newItem = true; 786 } catch (UserNotFoundException ex) { 787 Log.error("Group (" + group.getName() + ") includes non-existent username (" + 788 addedUser + 789 ")"); 790 return; 791 } 792 } 793 794 // If an item already exists then take note of the old subscription status 795 RosterItem.SubType prevSubscription = null; 796 if (!newItem) { 797 prevSubscription = item.getSubStatus(); 798 } 799 800 // Update the subscription of the item **based on the item groups** 801 Collection<Group> userGroups = GroupManager.getInstance().getGroups(getUserJID()); 802 Collection<Group> sharedGroups = new ArrayList<>(); 803 sharedGroups.addAll(item.getSharedGroups()); 804 // Add the new group to the list of groups to check 805 sharedGroups.add(group); 806 // Set subscription type to BOTH if the roster user belongs to a shared group 807 // that is mutually visible with a shared group of the new roster item 808 final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager(); 809 if (rosterManager.hasMutualVisibility(getUsername(), userGroups, addedUser, sharedGroups)) { 810 item.setSubStatus(RosterItem.SUB_BOTH); 811 } 812 // Update the subscription status depending on the group membership of the new 813 // user and this user 814 else if (group.isUser(addedUser) && !group.isUser(getUsername())) { 815 item.setSubStatus(RosterItem.SUB_TO); 816 } else if (!group.isUser(addedUser) && group.isUser(getUsername())) { 817 item.setSubStatus(RosterItem.SUB_FROM); 818 } 819 820 // Add the shared group to the list of shared groups 821 if (item.getSubStatus() != RosterItem.SUB_FROM) { 822 item.addSharedGroup(group); 823 } else { 824 item.addInvisibleSharedGroup(group); 825 } 826 827 // If the item already exists then check if the subscription status should be 828 // changed to BOTH based on the old and new subscription status 829 if (prevSubscription != null) { 830 if (prevSubscription == RosterItem.SUB_TO && 831 item.getSubStatus() == RosterItem.SUB_FROM) { 832 item.setSubStatus(RosterItem.SUB_BOTH); 833 } else if (prevSubscription == RosterItem.SUB_FROM && 834 item.getSubStatus() == RosterItem.SUB_TO) { 835 item.setSubStatus(RosterItem.SUB_BOTH); 836 } 837 } 838 839 // Optimization: Check if we do not need to keep the item in memory 840 if (item.isOnlyShared() && item.getSubStatus() == RosterItem.SUB_FROM) { 841 // Remove from memory and do nothing else 842 rosterItems.remove(item.getJid().toBareJID()); 843 // Cache information about shared contacts with subscription status FROM 844 implicitFrom.put(item.getJid().toBareJID(), item.getInvisibleSharedGroupsNames()); 845 } else { 846 // Remove from list of shared contacts with status FROM (if any) 847 implicitFrom.remove(item.getJid().toBareJID()); 848 // Ensure that the item is an explicit roster item 849 rosterItems.put(item.getJid().toBareJID(), item); 850 // Brodcast to all the user resources of the updated roster item 851 broadcast(item, true); 852 // Probe the presence of the new group user 853 if (item.getSubStatus() == RosterItem.SUB_BOTH || 854 item.getSubStatus() == RosterItem.SUB_TO) { 855 probePresence(item.getJid()); 856 } 857 } 858 if (newItem) { 859 // Fire event indicating that a roster item has been added 860 RosterEventDispatcher.contactAdded(this, item); 861 } else { 862 // Fire event indicating that a roster item has been updated 863 RosterEventDispatcher.contactUpdated(this, item); 864 } 865 } 866 867 /** 868 * Adds a new contact that belongs to a certain list of groups to the roster. Depending on 869 * the contact's groups and this user's groups, the presence subscription of the roster item may 870 * vary. 871 * 872 * @param addedUser the new contact to add to the roster 873 * @param groups the groups where the contact is a member 874 */ addSharedUser(JID addedUser, Collection<Group> groups, Group addedGroup)875 void addSharedUser(JID addedUser, Collection<Group> groups, Group addedGroup) { 876 boolean newItem; 877 RosterItem item; 878 try { 879 // Get the RosterItem for the *local* user to add 880 item = getRosterItem(addedUser); 881 newItem = false; 882 } catch (UserNotFoundException e) { 883 try { 884 // Create a new RosterItem for this new user 885 String nickname = UserNameManager.getUserName(addedUser); 886 item = 887 new RosterItem(addedUser, RosterItem.SUB_BOTH, RosterItem.ASK_NONE, 888 RosterItem.RECV_NONE, nickname, null); 889 // Add the new item to the list of items 890 rosterItems.put(item.getJid().toBareJID(), item); 891 newItem = true; 892 } catch (UserNotFoundException ex) { 893 Log.error("Couldn't find a user with username (" + addedUser + ")"); 894 return; 895 } 896 } 897 // Update the subscription of the item **based on the item groups** 898 Collection<Group> userGroups = GroupManager.getInstance().getGroups(getUserJID()); 899 // Set subscription type to BOTH if the roster user belongs to a shared group 900 // that is mutually visible with a shared group of the new roster item 901 final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager(); 902 if (rosterManager.hasMutualVisibility(getUsername(), userGroups, addedUser, groups)) { 903 item.setSubStatus(RosterItem.SUB_BOTH); 904 for (Group group : groups) { 905 if (rosterManager.isGroupVisible(group, getUserJID())) { 906 // Add the shared group to the list of shared groups 907 item.addSharedGroup(group); 908 } 909 } 910 // Add to the item the groups of this user that generated a FROM subscription 911 // Note: This FROM subscription is overridden by the BOTH subscription but in 912 // fact there is a TO-FROM relation between these two users that ends up in a 913 // BOTH subscription 914 for (Group group : userGroups) { 915 if (!group.isUser(addedUser) && rosterManager.isGroupVisible(group, addedUser)) { 916 // Add the shared group to the list of invisible shared groups 917 item.addInvisibleSharedGroup(group); 918 } 919 } 920 } else { 921 // If an item already exists then take note of the old subscription status 922 RosterItem.SubType prevSubscription = null; 923 if (!newItem) { 924 prevSubscription = item.getSubStatus(); 925 } 926 927 // Assume by default that the contact has subscribed from the presence of 928 // this user 929 item.setSubStatus(RosterItem.SUB_FROM); 930 // Check if the user may see the new contact in a shared group 931 for (Group group : groups) { 932 if (rosterManager.isGroupVisible(group, getUserJID())) { 933 // Add the shared group to the list of shared groups 934 item.addSharedGroup(group); 935 item.setSubStatus(RosterItem.SUB_TO); 936 } 937 } 938 if (item.getSubStatus() == RosterItem.SUB_FROM) { 939 item.addInvisibleSharedGroup(addedGroup); 940 } 941 942 // If the item already exists then check if the subscription status should be 943 // changed to BOTH based on the old and new subscription status 944 if (prevSubscription != null) { 945 if (prevSubscription == RosterItem.SUB_TO && 946 item.getSubStatus() == RosterItem.SUB_FROM) { 947 item.setSubStatus(RosterItem.SUB_BOTH); 948 } else if (prevSubscription == RosterItem.SUB_FROM && 949 item.getSubStatus() == RosterItem.SUB_TO) { 950 item.setSubStatus(RosterItem.SUB_BOTH); 951 } 952 } 953 } 954 // Optimization: Check if we do not need to keep the item in memory 955 if (item.isOnlyShared() && item.getSubStatus() == RosterItem.SUB_FROM) { 956 // Remove from memory and do nothing else 957 rosterItems.remove(item.getJid().toBareJID()); 958 // Cache information about shared contacts with subscription status FROM 959 implicitFrom.put(item.getJid().toBareJID(), item.getInvisibleSharedGroupsNames()); 960 } else { 961 // Remove from list of shared contacts with status FROM (if any) 962 implicitFrom.remove(item.getJid().toBareJID()); 963 // Ensure that the item is an explicit roster item 964 rosterItems.put(item.getJid().toBareJID(), item); 965 // Brodcast to all the user resources of the updated roster item 966 broadcast(item, true); 967 // Probe the presence of the new group user 968 if (item.getSubStatus() == RosterItem.SUB_BOTH || 969 item.getSubStatus() == RosterItem.SUB_TO) { 970 probePresence(item.getJid()); 971 } 972 } 973 if (newItem) { 974 // Fire event indicating that a roster item has been added 975 RosterEventDispatcher.contactAdded(this, item); 976 } else { 977 // Fire event indicating that a roster item has been updated 978 RosterEventDispatcher.contactUpdated(this, item); 979 } 980 } 981 982 /** 983 * Update the roster since a group user has been deleted from a shared group. If the RosterItem 984 * (of the deleted contact) exists only because of of the sahred group then the RosterItem will 985 * be deleted physically from the backend store. Otherwise the shared group will be removed from 986 * the shared groups lists. In any case an update broadcast will be sent to all the users 987 * logged resources. 988 * 989 * @param sharedGroup the shared group from where the user was deleted. 990 * @param deletedUser the contact to update in the roster. 991 */ deleteSharedUser(Group sharedGroup, JID deletedUser)992 void deleteSharedUser(Group sharedGroup, JID deletedUser) { 993 try { 994 // Get the RosterItem for the *local* user to remove 995 RosterItem item = getRosterItem(deletedUser); 996 int groupSize = item.getSharedGroups().size() + item.getInvisibleSharedGroups().size(); 997 if (item.isOnlyShared() && groupSize == 1) { 998 // Do nothing if the existing shared group is not the sharedGroup to remove 999 if (!item.getSharedGroups().contains(sharedGroup) && 1000 !item.getInvisibleSharedGroups().contains(sharedGroup)) { 1001 return; 1002 } 1003 // Delete the roster item from the roster since it exists only because of this 1004 // group which is being removed 1005 deleteRosterItem(deletedUser, false); 1006 } else { 1007 // Remove the removed shared group from the list of shared groups 1008 item.removeSharedGroup(sharedGroup); 1009 // Update the subscription of the item based on the remaining groups 1010 if (item.isOnlyShared()) { 1011 Collection<Group> userGroups = 1012 GroupManager.getInstance().getGroups(getUserJID()); 1013 Collection<Group> sharedGroups = new ArrayList<>(); 1014 sharedGroups.addAll(item.getSharedGroups()); 1015 // Set subscription type to BOTH if the roster user belongs to a shared group 1016 // that is mutually visible with a shared group of the new roster item 1017 final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager(); 1018 if (rosterManager.hasMutualVisibility(getUsername(), userGroups, deletedUser, 1019 sharedGroups)) { 1020 item.setSubStatus(RosterItem.SUB_BOTH); 1021 } else if (item.getSharedGroups().isEmpty() && 1022 !item.getInvisibleSharedGroups().isEmpty()) { 1023 item.setSubStatus(RosterItem.SUB_FROM); 1024 } else { 1025 item.setSubStatus(RosterItem.SUB_TO); 1026 } 1027 // Fire event indicating that a roster item has been updated 1028 RosterEventDispatcher.contactUpdated(this, item); 1029 } else { 1030 // Fire event indicating that a roster item has been removed 1031 RosterEventDispatcher.contactDeleted(this, item); 1032 } 1033 // Brodcast to all the user resources of the updated roster item 1034 broadcast(item, false); 1035 } 1036 } catch (SharedGroupException e) { 1037 // Do nothing. Checkings are disabled so this exception should never happen. 1038 Log.error( "Unexpected error while deleting user '{}' from shared group '{}'!", deletedUser, sharedGroup, e ); 1039 } catch (UserNotFoundException e) { 1040 // Do nothing since the contact does not exist in the user's roster. (strange case!) 1041 // Log.warn( "Unexpected error while deleting user '{}' from shared group '{}'!", deletedUser, sharedGroup, e ); 1042 } 1043 } 1044 deleteSharedUser(JID deletedUser, Group deletedGroup)1045 void deleteSharedUser(JID deletedUser, Group deletedGroup) { 1046 try { 1047 final RosterManager rosterManager = XMPPServer.getInstance().getRosterManager(); 1048 // Get the RosterItem for the *local* user to remove 1049 RosterItem item = getRosterItem(deletedUser); 1050 int groupSize = item.getSharedGroups().size() + item.getInvisibleSharedGroups().size(); 1051 if (item.isOnlyShared() && groupSize == 1 && 1052 // Do not delete the item if deletedUser belongs to a public group since the 1053 // subcription status will change 1054 !(deletedGroup.isUser(deletedUser) && 1055 RosterManager.isPublicSharedGroup(deletedGroup))) { 1056 // Delete the roster item from the roster since it exists only because of this 1057 // group which is being removed 1058 deleteRosterItem(deletedUser, false); 1059 } else { 1060 // Remove the shared group from the item if deletedUser does not belong to a 1061 // public group 1062 if (!(deletedGroup.isUser(deletedUser) && 1063 RosterManager.isPublicSharedGroup(deletedGroup))) { 1064 item.removeSharedGroup(deletedGroup); 1065 } 1066 // Get the groups of the deleted user 1067 Collection<Group> groups = GroupManager.getInstance().getGroups(deletedUser); 1068 // Remove all invalid shared groups from the roster item 1069 for (Group group : groups) { 1070 if (!rosterManager.isGroupVisible(group, getUserJID())) { 1071 // Remove the shared group from the list of shared groups 1072 item.removeSharedGroup(group); 1073 } 1074 } 1075 1076 // Update the subscription of the item **based on the item groups** 1077 if (item.isOnlyShared()) { 1078 Collection<Group> userGroups = 1079 GroupManager.getInstance().getGroups(getUserJID()); 1080 // Set subscription type to BOTH if the roster user belongs to a shared group 1081 // that is mutually visible with a shared group of the new roster item 1082 if (rosterManager 1083 .hasMutualVisibility(getUsername(), userGroups, deletedUser, groups)) { 1084 item.setSubStatus(RosterItem.SUB_BOTH); 1085 } else { 1086 // Assume by default that the contact has subscribed from the presence of 1087 // this user 1088 item.setSubStatus(RosterItem.SUB_FROM); 1089 // Check if the user may see the new contact in a shared group 1090 for (Group group : groups) { 1091 if (rosterManager.isGroupVisible(group, getUserJID())) { 1092 item.setSubStatus(RosterItem.SUB_TO); 1093 } 1094 } 1095 } 1096 // Fire event indicating that a roster item has been updated 1097 RosterEventDispatcher.contactUpdated(this, item); 1098 } else { 1099 // Fire event indicating that a roster item has been removed 1100 RosterEventDispatcher.contactDeleted(this, item); 1101 } 1102 // Brodcast to all the user resources of the updated roster item 1103 broadcast(item, false); 1104 } 1105 } catch (SharedGroupException e) { 1106 // Do nothing. Checkings are disabled so this exception should never happen. 1107 Log.error( "Unexpected error while deleting user '{}' from shared group '{}'!", deletedUser, deletedGroup, e); 1108 } catch (UserNotFoundException e) { 1109 // Do nothing since the contact does not exist in the user's roster. (strange case!) 1110 // Log.warn( "Unexpected error while deleting user '{}' from shared group '{}'!", deletedUser, deletedGroup, e); 1111 } 1112 } 1113 1114 /** 1115 * A shared group of the user has been renamed. Update the existing roster items with the new 1116 * name of the shared group and make a roster push for all the available resources. 1117 * 1118 * @param users group users of the renamed group. 1119 */ shareGroupRenamed(Collection<JID> users)1120 void shareGroupRenamed(Collection<JID> users) { 1121 JID userJID = getUserJID(); 1122 for (JID user : users) { 1123 if (userJID.equals(user)) { 1124 continue; 1125 } 1126 RosterItem item; 1127 try { 1128 // Get the RosterItem for the *local* user to add 1129 item = getRosterItem(user); 1130 // Broadcast to all the user resources of the updated roster item 1131 broadcast(item, true); 1132 } catch (UserNotFoundException e) { 1133 // Do nothing since the contact does not exist in the user's roster. (strange case!) 1134 // Log.warn( "Unexpected error while broadcasting shared group rename for user '{}'!", user, e); 1135 } 1136 } 1137 } 1138 getUserJID()1139 private JID getUserJID() { 1140 return XMPPServer.getInstance().createJID(getUsername(), null, true); 1141 } 1142 1143 @Override equals( Object o )1144 public boolean equals( Object o ) 1145 { 1146 if ( this == o ) 1147 { 1148 return true; 1149 } 1150 if ( o == null || getClass() != o.getClass() ) 1151 { 1152 return false; 1153 } 1154 1155 final Roster roster = (Roster) o; 1156 1157 if ( !rosterItems.equals( roster.rosterItems ) ) 1158 { 1159 return false; 1160 } 1161 if ( !implicitFrom.equals( roster.implicitFrom ) ) 1162 { 1163 return false; 1164 } 1165 return username.equals( roster.username ); 1166 } 1167 1168 @Override hashCode()1169 public int hashCode() 1170 { 1171 int result = rosterItems.hashCode(); 1172 result = 31 * result + implicitFrom.hashCode(); 1173 result = 31 * result + username.hashCode(); 1174 return result; 1175 } 1176 1177 @Override writeExternal(ObjectOutput out)1178 public void writeExternal(ObjectOutput out) throws IOException { 1179 ExternalizableUtil.getInstance().writeSafeUTF(out, username); 1180 ExternalizableUtil.getInstance().writeExternalizableMap(out, rosterItems); 1181 ExternalizableUtil.getInstance().writeStringsMap(out, implicitFrom); 1182 } 1183 1184 @Override readExternal(ObjectInput in)1185 public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { 1186 username = ExternalizableUtil.getInstance().readSafeUTF(in); 1187 ExternalizableUtil.getInstance().readExternalizableMap(in, rosterItems, getClass().getClassLoader()); 1188 ExternalizableUtil.getInstance().readStringsMap(in, implicitFrom); 1189 } 1190 } 1191