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