001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smack.roster;
019
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashSet;
025import java.util.LinkedHashSet;
026import java.util.List;
027import java.util.Locale;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Set;
031import java.util.WeakHashMap;
032import java.util.concurrent.ConcurrentHashMap;
033import java.util.concurrent.CopyOnWriteArraySet;
034import java.util.logging.Level;
035import java.util.logging.Logger;
036
037import org.jivesoftware.smack.AbstractConnectionClosedListener;
038import org.jivesoftware.smack.ConnectionCreationListener;
039import org.jivesoftware.smack.ExceptionCallback;
040import org.jivesoftware.smack.Manager;
041import org.jivesoftware.smack.StanzaListener;
042import org.jivesoftware.smack.SmackException;
043import org.jivesoftware.smack.XMPPConnection;
044import org.jivesoftware.smack.SmackException.NoResponseException;
045import org.jivesoftware.smack.SmackException.NotConnectedException;
046import org.jivesoftware.smack.SmackException.NotLoggedInException;
047import org.jivesoftware.smack.XMPPConnectionRegistry;
048import org.jivesoftware.smack.XMPPException.XMPPErrorException;
049import org.jivesoftware.smack.filter.StanzaFilter;
050import org.jivesoftware.smack.filter.StanzaTypeFilter;
051import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
052import org.jivesoftware.smack.packet.IQ;
053import org.jivesoftware.smack.packet.IQ.Type;
054import org.jivesoftware.smack.packet.Stanza;
055import org.jivesoftware.smack.packet.Presence;
056import org.jivesoftware.smack.packet.XMPPError;
057import org.jivesoftware.smack.packet.XMPPError.Condition;
058import org.jivesoftware.smack.roster.packet.RosterPacket;
059import org.jivesoftware.smack.roster.packet.RosterVer;
060import org.jivesoftware.smack.roster.packet.RosterPacket.Item;
061import org.jivesoftware.smack.roster.rosterstore.RosterStore;
062import org.jivesoftware.smack.util.Objects;
063import org.jxmpp.util.XmppStringUtils;
064
065/**
066 * Represents a user's roster, which is the collection of users a person receives
067 * presence updates for. Roster items are categorized into groups for easier management.<p>
068 * <p/>
069 * Others users may attempt to subscribe to this user using a subscription request. Three
070 * modes are supported for handling these requests: <ul>
071 * <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li>
072 * <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li>
073 * <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li>
074 * </ul>
075 *
076 * @author Matt Tucker
077 * @see #getInstanceFor(XMPPConnection)
078 */
079public class Roster extends Manager {
080
081    private static final Logger LOGGER = Logger.getLogger(Roster.class.getName());
082
083    static {
084        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
085            @Override
086            public void connectionCreated(XMPPConnection connection) {
087                getInstanceFor(connection);
088            }
089        });
090    }
091
092    private static final Map<XMPPConnection, Roster> INSTANCES = new WeakHashMap<>();
093
094    /**
095     * Returns the roster for the user.
096     * <p>
097     * This method will never return <code>null</code>, instead if the user has not yet logged into
098     * the server or is logged in anonymously all modifying methods of the returned roster object
099     * like {@link Roster#createEntry(String, String, String[])},
100     * {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing
101     * {@link RosterListener}s will throw an IllegalStateException.
102     * 
103     * @return the user's roster.
104     * @throws IllegalStateException if the connection is anonymous
105     */
106    public static synchronized Roster getInstanceFor(XMPPConnection connection) {
107        Roster roster = INSTANCES.get(connection);
108        if (roster == null) {
109            roster = new Roster(connection);
110            INSTANCES.put(connection, roster);
111        }
112        return roster;
113    }
114
115    private static final StanzaFilter PRESENCE_PACKET_FILTER = StanzaTypeFilter.PRESENCE;
116
117    private static boolean rosterLoadedAtLoginDefault = true;
118
119    /**
120     * The default subscription processing mode to use when a Roster is created. By default
121     * all subscription requests are automatically accepted.
122     */
123    private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.accept_all;
124
125    private RosterStore rosterStore;
126    private final Map<String, RosterGroup> groups = new ConcurrentHashMap<String, RosterGroup>();
127
128    /**
129     * Concurrent hash map from JID to its roster entry.
130     */
131    private final Map<String,RosterEntry> entries = new ConcurrentHashMap<String,RosterEntry>();
132
133    private final Set<RosterEntry> unfiledEntries = new CopyOnWriteArraySet<>();
134    private final Set<RosterListener> rosterListeners = new LinkedHashSet<>();
135    private final Map<String, Map<String, Presence>> presenceMap = new ConcurrentHashMap<String, Map<String, Presence>>();
136
137    /**
138     * Listeners called when the Roster was loaded.
139     */
140    private final Set<RosterLoadedListener> rosterLoadedListeners = new LinkedHashSet<>();
141
142    /**
143     * Mutually exclude roster listener invocation and changing the {@link entries} map. Also used
144     * to synchronize access to either the roster listeners or the entries map.
145     */
146    private final Object rosterListenersAndEntriesLock = new Object();
147
148    private enum RosterState {
149        uninitialized,
150        loading,
151        loaded,
152    }
153
154    /**
155     * The current state of the roster.
156     */
157    private RosterState rosterState = RosterState.uninitialized;
158
159    private final PresencePacketListener presencePacketListener = new PresencePacketListener();
160
161    /**
162     * 
163     */
164    private boolean rosterLoadedAtLogin = rosterLoadedAtLoginDefault;
165
166    private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode();
167
168    /**
169     * Returns the default subscription processing mode to use when a new Roster is created. The
170     * subscription processing mode dictates what action Smack will take when subscription
171     * requests from other users are made. The default subscription mode
172     * is {@link SubscriptionMode#accept_all}.
173     *
174     * @return the default subscription mode to use for new Rosters
175     */
176    public static SubscriptionMode getDefaultSubscriptionMode() {
177        return defaultSubscriptionMode;
178    }
179
180    /**
181     * Sets the default subscription processing mode to use when a new Roster is created. The
182     * subscription processing mode dictates what action Smack will take when subscription
183     * requests from other users are made. The default subscription mode
184     * is {@link SubscriptionMode#accept_all}.
185     *
186     * @param subscriptionMode the default subscription mode to use for new Rosters.
187     */
188    public static void setDefaultSubscriptionMode(SubscriptionMode subscriptionMode) {
189        defaultSubscriptionMode = subscriptionMode;
190    }
191
192    /**
193     * Creates a new roster.
194     *
195     * @param connection an XMPP connection.
196     */
197    private Roster(final XMPPConnection connection) {
198        super(connection);
199
200        // Note that we use sync packet listeners because RosterListeners should be invoked in the same order as the
201        // roster stanzas arrive.
202        // Listen for any roster packets.
203        connection.registerIQRequestHandler(new RosterPushListener());
204        // Listen for any presence packets.
205        connection.addSyncStanzaListener(presencePacketListener, PRESENCE_PACKET_FILTER);
206
207        // Listen for connection events
208        connection.addConnectionListener(new AbstractConnectionClosedListener() {
209
210            @Override
211            public void authenticated(XMPPConnection connection, boolean resumed) {
212                // Anonymous users can't have a roster, but it is possible that a Roster instance is
213                // retrieved if getRoster() is called *before* connect(). So we have to check here
214                // again if it's an anonymous connection.
215                if (connection.isAnonymous())
216                    return;
217                if (!isRosterLoadedAtLogin())
218                    return;
219                // We are done here if the connection was resumed
220                if (resumed) {
221                    return;
222                }
223                try {
224                    Roster.this.reload();
225                }
226                catch (SmackException e) {
227                    LOGGER.log(Level.SEVERE, "Could not reload Roster", e);
228                    return;
229                }
230            }
231
232            @Override
233            public void connectionTerminated() {
234                // Changes the presence available contacts to unavailable
235                setOfflinePresencesAndResetLoaded();
236            }
237
238        });
239        // If the connection is already established, call reload
240        if (connection.isAuthenticated()) {
241            try {
242                reload();
243            }
244            catch (SmackException e) {
245                LOGGER.log(Level.SEVERE, "Could not reload Roster", e);
246            }
247        }
248    }
249
250    /**
251     * Returns the subscription processing mode, which dictates what action
252     * Smack will take when subscription requests from other users are made.
253     * The default subscription mode is {@link SubscriptionMode#accept_all}.<p>
254     * <p/>
255     * If using the manual mode, a PacketListener should be registered that
256     * listens for Presence packets that have a type of
257     * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}.
258     *
259     * @return the subscription mode.
260     */
261    public SubscriptionMode getSubscriptionMode() {
262        return subscriptionMode;
263    }
264
265    /**
266     * Sets the subscription processing mode, which dictates what action
267     * Smack will take when subscription requests from other users are made.
268     * The default subscription mode is {@link SubscriptionMode#accept_all}.<p>
269     * <p/>
270     * If using the manual mode, a PacketListener should be registered that
271     * listens for Presence packets that have a type of
272     * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}.
273     *
274     * @param subscriptionMode the subscription mode.
275     */
276    public void setSubscriptionMode(SubscriptionMode subscriptionMode) {
277        this.subscriptionMode = subscriptionMode;
278    }
279
280    /**
281     * Reloads the entire roster from the server. This is an asynchronous operation,
282     * which means the method will return immediately, and the roster will be
283     * reloaded at a later point when the server responds to the reload request.
284     * @throws NotLoggedInException If not logged in.
285     * @throws NotConnectedException 
286     */
287    public void reload() throws NotLoggedInException, NotConnectedException{
288        final XMPPConnection connection = connection();
289        if (!connection.isAuthenticated()) {
290            throw new NotLoggedInException();
291        }
292        if (connection.isAnonymous()) {
293            throw new IllegalStateException("Anonymous users can't have a roster.");
294        }
295
296        RosterPacket packet = new RosterPacket();
297        if (rosterStore != null && isRosterVersioningSupported()) {
298            packet.setVersion(rosterStore.getRosterVersion());
299        }
300        rosterState = RosterState.loading;
301        connection.sendIqWithResponseCallback(packet, new RosterResultListener(), new ExceptionCallback() {
302            @Override
303            public void processException(Exception exception) {
304                rosterState = RosterState.uninitialized;
305                LOGGER.log(Level.SEVERE, "Exception reloading roster" , exception);
306            }
307        });
308    }
309
310    /**
311     * Reload the roster and block until it is reloaded.
312     *
313     * @throws NotLoggedInException
314     * @throws NotConnectedException
315     * @throws InterruptedException 
316     * @since 4.1
317     */
318    public void reloadAndWait() throws NotLoggedInException, NotConnectedException, InterruptedException {
319        reload();
320        waitUntilLoaded();
321    }
322 
323    /**
324     * Set the roster store, may cause a roster reload
325     *
326     * @param rosterStore
327     * @return true if the roster reload was initiated, false otherwise.
328     * @since 4.1
329     */
330    public boolean setRosterStore(RosterStore rosterStore) {
331        this.rosterStore = rosterStore;
332        try {
333            reload();
334        }
335        catch (NotLoggedInException | NotConnectedException e) {
336            LOGGER.log(Level.FINER, "Could not reload roster", e);
337            return false;
338        }
339        return true;
340    }
341
342    protected boolean waitUntilLoaded() throws InterruptedException {
343        long waitTime = connection().getPacketReplyTimeout();
344        long start = System.currentTimeMillis();
345        while (!isLoaded()) {
346            if (waitTime <= 0) {
347                break;
348            }
349            synchronized (this) {
350                if (!isLoaded()) {
351                    wait(waitTime);
352                }
353            }
354            long now = System.currentTimeMillis();
355            waitTime -= now - start;
356            start = now;
357        }
358        return isLoaded();
359    }
360
361    /**
362     * Check if the roster is loaded.
363     *
364     * @return true if the roster is loaded.
365     * @since 4.1
366     */
367    public boolean isLoaded() {
368        return rosterState == RosterState.loaded;
369    }
370
371    /**
372     * Adds a listener to this roster. The listener will be fired anytime one or more
373     * changes to the roster are pushed from the server.
374     *
375     * @param rosterListener a roster listener.
376     * @return true if the listener was not already added.
377     * @see #getEntriesAndAddListener(RosterListener, RosterEntries)
378     */
379    public boolean addRosterListener(RosterListener rosterListener) {
380        synchronized (rosterListenersAndEntriesLock) {
381            return rosterListeners.add(rosterListener);
382        }
383    }
384
385    /**
386     * Removes a listener from this roster. The listener will be fired anytime one or more
387     * changes to the roster are pushed from the server.
388     *
389     * @param rosterListener a roster listener.
390     * @return true if the listener was active and got removed.
391     */
392    public boolean removeRosterListener(RosterListener rosterListener) {
393        synchronized (rosterListenersAndEntriesLock) {
394            return rosterListeners.remove(rosterListener);
395        }
396    }
397
398    /**
399     * Add a roster loaded listener.
400     *
401     * @param rosterLoadedListener the listener to add.
402     * @return true if the listener was not already added.
403     * @see RosterLoadedListener
404     * @since 4.1
405     */
406    public boolean addRosterLoadedListener(RosterLoadedListener rosterLoadedListener) {
407        synchronized (rosterLoadedListener) {
408            return rosterLoadedListeners.add(rosterLoadedListener);
409        }
410    }
411
412    /**
413     * Remove a roster loaded listener.
414     *
415     * @param rosterLoadedListener the listener to remove.
416     * @return true if the listener was active and got removed.
417     * @see RosterLoadedListener
418     * @since 4.1
419     */
420    public boolean removeRosterLoadedListener(RosterLoadedListener rosterLoadedListener) {
421        synchronized (rosterLoadedListener) {
422            return rosterLoadedListeners.remove(rosterLoadedListener);
423        }
424    }
425
426    /**
427     * Creates a new group.<p>
428     * <p/>
429     * Note: you must add at least one entry to the group for the group to be kept
430     * after a logout/login. This is due to the way that XMPP stores group information.
431     *
432     * @param name the name of the group.
433     * @return a new group, or null if the group already exists
434     * @throws IllegalStateException if logged in anonymously
435     */
436    public RosterGroup createGroup(String name) {
437        final XMPPConnection connection = connection();
438        if (connection.isAnonymous()) {
439            throw new IllegalStateException("Anonymous users can't have a roster.");
440        }
441        if (groups.containsKey(name)) {
442            return groups.get(name);
443        }
444        
445        RosterGroup group = new RosterGroup(name, connection);
446        groups.put(name, group);
447        return group;
448    }
449
450    /**
451     * Creates a new roster entry and presence subscription. The server will asynchronously
452     * update the roster with the subscription status.
453     *
454     * @param user   the user. (e.g. johndoe@jabber.org)
455     * @param name   the nickname of the user.
456     * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the
457     *               the roster entry won't belong to a group.
458     * @throws NoResponseException if there was no response from the server.
459     * @throws XMPPErrorException if an XMPP exception occurs.
460     * @throws NotLoggedInException If not logged in.
461     * @throws NotConnectedException 
462     */
463    public void createEntry(String user, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException {
464        final XMPPConnection connection = connection();
465        if (!connection.isAuthenticated()) {
466            throw new NotLoggedInException();
467        }
468        if (connection.isAnonymous()) {
469            throw new IllegalStateException("Anonymous users can't have a roster.");
470        }
471
472        // Create and send roster entry creation packet.
473        RosterPacket rosterPacket = new RosterPacket();
474        rosterPacket.setType(IQ.Type.set);
475        RosterPacket.Item item = new RosterPacket.Item(user, name);
476        if (groups != null) {
477            for (String group : groups) {
478                if (group != null && group.trim().length() > 0) {
479                    item.addGroupName(group);
480                }
481            }
482        }
483        rosterPacket.addRosterItem(item);
484        connection.createPacketCollectorAndSend(rosterPacket).nextResultOrThrow();
485
486        // Create a presence subscription packet and send.
487        Presence presencePacket = new Presence(Presence.Type.subscribe);
488        presencePacket.setTo(user);
489        connection.sendStanza(presencePacket);
490    }
491
492    /**
493     * Removes a roster entry from the roster. The roster entry will also be removed from the
494     * unfiled entries or from any roster group where it could belong and will no longer be part
495     * of the roster. Note that this is a synchronous call -- Smack must wait for the server
496     * to send an updated subscription status.
497     *
498     * @param entry a roster entry.
499     * @throws XMPPErrorException if an XMPP error occurs.
500     * @throws NotLoggedInException if not logged in.
501     * @throws NoResponseException SmackException if there was no response from the server.
502     * @throws NotConnectedException 
503     * @throws IllegalStateException if connection is not logged in or logged in anonymously
504     */
505    public void removeEntry(RosterEntry entry) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException {
506        final XMPPConnection connection = connection();
507        if (!connection.isAuthenticated()) {
508            throw new NotLoggedInException();
509        }
510        if (connection.isAnonymous()) {
511            throw new IllegalStateException("Anonymous users can't have a roster.");
512        }
513
514        // Only remove the entry if it's in the entry list.
515        // The actual removal logic takes place in RosterPacketListenerprocess>>Packet(Packet)
516        if (!entries.containsKey(entry.getUser())) {
517            return;
518        }
519        RosterPacket packet = new RosterPacket();
520        packet.setType(IQ.Type.set);
521        RosterPacket.Item item = RosterEntry.toRosterItem(entry);
522        // Set the item type as REMOVE so that the server will delete the entry
523        item.setItemType(RosterPacket.ItemType.remove);
524        packet.addRosterItem(item);
525        connection.createPacketCollectorAndSend(packet).nextResultOrThrow();
526    }
527
528    /**
529     * Returns a count of the entries in the roster.
530     *
531     * @return the number of entries in the roster.
532     */
533    public int getEntryCount() {
534        return getEntries().size();
535    }
536
537    /**
538     * Add a roster listener and invoke the roster entries with all entries of the roster.
539     * <p>
540     * The method guarantees that the listener is only invoked after
541     * {@link RosterEntries#rosterEntires(Collection)} has been invoked, and that all roster events
542     * that happen while <code>rosterEntires(Collection) </code> is called are queued until the
543     * method returns.
544     * </p>
545     * <p>
546     * This guarantee makes this the ideal method to e.g. populate a UI element with the roster while
547     * installing a {@link RosterListener} to listen for subsequent roster events.
548     * </p>
549     *
550     * @param rosterListener the listener to install
551     * @param rosterEntries the roster entries callback interface
552     * @since 4.1
553     */
554    public void getEntriesAndAddListener(RosterListener rosterListener, RosterEntries rosterEntries) {
555        Objects.requireNonNull(rosterListener, "listener must not be null");
556        Objects.requireNonNull(rosterEntries, "rosterEntries must not be null");
557
558        synchronized (rosterListenersAndEntriesLock) {
559            rosterEntries.rosterEntires(entries.values());
560            addRosterListener(rosterListener);
561        }
562    }
563
564    /**
565     * Returns a set of all entries in the roster, including entries
566     * that don't belong to any groups.
567     *
568     * @return all entries in the roster.
569     */
570    public Set<RosterEntry> getEntries() {
571        Set<RosterEntry> allEntries;
572        synchronized (rosterListenersAndEntriesLock) {
573            allEntries = new HashSet<>(entries.size());
574            for (RosterEntry entry : entries.values()) {
575                allEntries.add(entry);
576            }
577        }
578        return allEntries;
579    }
580
581    /**
582     * Returns a count of the unfiled entries in the roster. An unfiled entry is
583     * an entry that doesn't belong to any groups.
584     *
585     * @return the number of unfiled entries in the roster.
586     */
587    public int getUnfiledEntryCount() {
588        return unfiledEntries.size();
589    }
590
591    /**
592     * Returns an unmodifiable set for the unfiled roster entries. An unfiled entry is
593     * an entry that doesn't belong to any groups.
594     *
595     * @return the unfiled roster entries.
596     */
597    public Set<RosterEntry> getUnfiledEntries() {
598        return Collections.unmodifiableSet(unfiledEntries);
599    }
600
601    /**
602     * Returns the roster entry associated with the given XMPP address or
603     * <tt>null</tt> if the user is not an entry in the roster.
604     *
605     * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be
606     *             in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource").
607     * @return the roster entry or <tt>null</tt> if it does not exist.
608     */
609    public RosterEntry getEntry(String user) {
610        if (user == null) {
611            return null;
612        }
613        String key = getMapKey(user);
614        return entries.get(key);
615    }
616
617    /**
618     * Returns true if the specified XMPP address is an entry in the roster.
619     *
620     * @param user the XMPP address of the user (eg "jsmith@example.com"). The
621     *             address could be in any valid format (e.g. "domain/resource",
622     *             "user@domain" or "user@domain/resource").
623     * @return true if the XMPP address is an entry in the roster.
624     */
625    public boolean contains(String user) {
626        return getEntry(user) != null;
627    }
628
629    /**
630     * Returns the roster group with the specified name, or <tt>null</tt> if the
631     * group doesn't exist.
632     *
633     * @param name the name of the group.
634     * @return the roster group with the specified name.
635     */
636    public RosterGroup getGroup(String name) {
637        return groups.get(name);
638    }
639
640    /**
641     * Returns the number of the groups in the roster.
642     *
643     * @return the number of groups in the roster.
644     */
645    public int getGroupCount() {
646        return groups.size();
647    }
648
649    /**
650     * Returns an unmodifiable collections of all the roster groups.
651     *
652     * @return an iterator for all roster groups.
653     */
654    public Collection<RosterGroup> getGroups() {
655        return Collections.unmodifiableCollection(groups.values());
656    }
657
658    /**
659     * Returns the presence info for a particular user. If the user is offline, or
660     * if no presence data is available (such as when you are not subscribed to the
661     * user's presence updates), unavailable presence will be returned.
662     * <p>
663     * If the user has several presences (one for each resource), then the presence with
664     * highest priority will be returned. If multiple presences have the same priority,
665     * the one with the "most available" presence mode will be returned. In order,
666     * that's {@link org.jivesoftware.smack.packet.Presence.Mode#chat free to chat},
667     * {@link org.jivesoftware.smack.packet.Presence.Mode#available available},
668     * {@link org.jivesoftware.smack.packet.Presence.Mode#away away},
669     * {@link org.jivesoftware.smack.packet.Presence.Mode#xa extended away}, and
670     * {@link org.jivesoftware.smack.packet.Presence.Mode#dnd do not disturb}.<p>
671     * </p>
672     * <p>
673     * Note that presence information is received asynchronously. So, just after logging
674     * in to the server, presence values for users in the roster may be unavailable
675     * even if they are actually online. In other words, the value returned by this
676     * method should only be treated as a snapshot in time, and may not accurately reflect
677     * other user's presence instant by instant. If you need to track presence over time,
678     * such as when showing a visual representation of the roster, consider using a
679     * {@link RosterListener}.
680     * </p>
681     *
682     * @param user an XMPP ID. The address could be in any valid format (e.g.
683     *             "domain/resource", "user@domain" or "user@domain/resource"). Any resource
684     *             information that's part of the ID will be discarded.
685     * @return the user's current presence, or unavailable presence if the user is offline
686     *         or if no presence information is available..
687     */
688    public Presence getPresence(String user) {
689        String key = getMapKey(XmppStringUtils.parseBareJid(user));
690        Map<String, Presence> userPresences = presenceMap.get(key);
691        if (userPresences == null) {
692            Presence presence = new Presence(Presence.Type.unavailable);
693            presence.setFrom(user);
694            return presence;
695        }
696        else {
697            // Find the resource with the highest priority
698            // Might be changed to use the resource with the highest availability instead.
699            Presence presence = null;
700            // This is used in case no available presence is found
701            Presence unavailable = null;
702
703            for (String resource : userPresences.keySet()) {
704                Presence p = userPresences.get(resource);
705                if (!p.isAvailable()) {
706                    unavailable = p;
707                    continue;
708                }
709                // Chose presence with highest priority first.
710                if (presence == null || p.getPriority() > presence.getPriority()) {
711                    presence = p;
712                }
713                // If equal priority, choose "most available" by the mode value.
714                else if (p.getPriority() == presence.getPriority()) {
715                    Presence.Mode pMode = p.getMode();
716                    // Default to presence mode of available.
717                    if (pMode == null) {
718                        pMode = Presence.Mode.available;
719                    }
720                    Presence.Mode presenceMode = presence.getMode();
721                    // Default to presence mode of available.
722                    if (presenceMode == null) {
723                        presenceMode = Presence.Mode.available;
724                    }
725                    if (pMode.compareTo(presenceMode) < 0) {
726                        presence = p;
727                    }
728                }
729            }
730            if (presence == null) {
731                if (unavailable != null) {
732                    return unavailable.clone();
733                }
734                else {
735                    presence = new Presence(Presence.Type.unavailable);
736                    presence.setFrom(user);
737                    return presence;
738                }
739            }
740            else {
741                return presence.clone();
742            }
743        }
744    }
745
746    /**
747     * Returns the presence info for a particular user's resource, or unavailable presence
748     * if the user is offline or if no presence information is available, such as
749     * when you are not subscribed to the user's presence updates.
750     *
751     * @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource).
752     * @return the user's current presence, or unavailable presence if the user is offline
753     *         or if no presence information is available.
754     */
755    public Presence getPresenceResource(String userWithResource) {
756        String key = getMapKey(userWithResource);
757        String resource = XmppStringUtils.parseResource(userWithResource);
758        Map<String, Presence> userPresences = presenceMap.get(key);
759        if (userPresences == null) {
760            Presence presence = new Presence(Presence.Type.unavailable);
761            presence.setFrom(userWithResource);
762            return presence;
763        }
764        else {
765            Presence presence = userPresences.get(resource);
766            if (presence == null) {
767                presence = new Presence(Presence.Type.unavailable);
768                presence.setFrom(userWithResource);
769                return presence;
770            }
771            else {
772                return presence.clone();
773            }
774        }
775    }
776
777    /**
778     * Returns a List of Presence objects for all of a user's current presences if no presence information is available,
779     * such as when you are not subscribed to the user's presence updates.
780     *
781     * @param bareJid an XMPP ID, e.g. jdoe@example.com.
782     * @return a List of Presence objects for all the user's current presences, or an unavailable presence if no
783     *         presence information is available.
784     */
785    public List<Presence> getAllPresences(String bareJid) {
786        Map<String, Presence> userPresences = presenceMap.get(getMapKey(bareJid));
787        List<Presence> res;
788        if (userPresences == null) {
789            // Create an unavailable presence if none was found
790            Presence unavailable = new Presence(Presence.Type.unavailable);
791            unavailable.setFrom(bareJid);
792            res = new ArrayList<>(Arrays.asList(unavailable));
793        } else {
794            res = new ArrayList<>(userPresences.values().size());
795            for (Presence presence : userPresences.values()) {
796                res.add(presence.clone());
797            }
798        }
799        return res;
800    }
801
802    /**
803     * Returns a List of all <b>available</b> Presence Objects for the given bare JID. If there are no available
804     * presences, then the empty list will be returned.
805     *
806     * @param bareJid the bare JID from which the presences should be retrieved.
807     * @return available presences for the bare JID.
808     */
809    public List<Presence> getAvailablePresences(String bareJid) {
810        List<Presence> allPresences = getAllPresences(bareJid);
811        List<Presence> res = new ArrayList<>(allPresences.size());
812        for (Presence presence : allPresences) {
813            if (presence.isAvailable()) {
814                // No need to clone presence here, getAllPresences already returns clones
815                res.add(presence);
816            }
817        }
818        return res;
819    }
820
821    /**
822     * Returns a List of Presence objects for all of a user's current presences
823     * or an unavailable presence if the user is unavailable (offline) or if no presence
824     * information is available, such as when you are not subscribed to the user's presence
825     * updates.
826     *
827     * @param user an XMPP ID, e.g. jdoe@example.com.
828     * @return a List of Presence objects for all the user's current presences,
829     *         or an unavailable presence if the user is offline or if no presence information
830     *         is available.
831     */
832    public List<Presence> getPresences(String user) {
833        List<Presence> res;
834        String key = getMapKey(user);
835        Map<String, Presence> userPresences = presenceMap.get(key);
836        if (userPresences == null) {
837            Presence presence = new Presence(Presence.Type.unavailable);
838            presence.setFrom(user);
839            res = Arrays.asList(presence);
840        }
841        else {
842            List<Presence> answer = new ArrayList<Presence>();
843            // Used in case no available presence is found
844            Presence unavailable = null;
845            for (Presence presence : userPresences.values()) {
846                if (presence.isAvailable()) {
847                    answer.add(presence.clone());
848                }
849                else {
850                    unavailable = presence;
851                }
852            }
853            if (!answer.isEmpty()) {
854                res = answer;
855            }
856            else if (unavailable != null) {
857                res = Arrays.asList(unavailable.clone());
858            }
859            else {
860                Presence presence = new Presence(Presence.Type.unavailable);
861                presence.setFrom(user);
862                res = Arrays.asList(presence);
863            }
864        }
865        return res;
866    }
867
868    /**
869     * Check if the given JID is subscribed to the user's presence.
870     * <p>
871     * If the JID is subscribed to the user's presence then it is allowed to see the presence and
872     * will get notified about presence changes. Also returns true, if the JID is the service
873     * name of the XMPP connection (the "XMPP domain"), i.e. the XMPP service is treated like
874     * having an implicit subscription to the users presence.
875     * </p>
876     * Note that if the roster is not loaded, then this method will always return false.
877     * 
878     * @param jid
879     * @return true if the given JID is allowed to see the users presence.
880     * @since 4.1
881     */
882    public boolean isSubscribedToMyPresence(String jid) {
883        if (connection().getServiceName().equals(jid)) {
884            return true;
885        }
886        RosterEntry entry = getEntry(jid);
887        if (entry == null) {
888            return false;
889        }
890        switch (entry.getType()) {
891        case from:
892        case both:
893            return true;
894        default:
895            return false;
896        }
897    }
898
899    /**
900     * Sets if the roster will be loaded from the server when logging in. This
901     * is the common behaviour for clients but sometimes clients may want to differ this
902     * or just never do it if not interested in rosters.
903     *
904     * @param rosterLoadedAtLogin if the roster will be loaded from the server when logging in.
905     */
906    public void setRosterLoadedAtLogin(boolean rosterLoadedAtLogin) {
907        this.rosterLoadedAtLogin = rosterLoadedAtLogin;
908    }
909
910    /**
911     * Returns true if the roster will be loaded from the server when logging in. This
912     * is the common behavior for clients but sometimes clients may want to differ this
913     * or just never do it if not interested in rosters.
914     *
915     * @return true if the roster will be loaded from the server when logging in.
916     * @see <a href="http://xmpp.org/rfcs/rfc6121.html#roster-login">RFC 6121 2.2 - Retrieving the Roster on Login</a>
917     */
918    public boolean isRosterLoadedAtLogin() {
919        return rosterLoadedAtLogin;
920    }
921
922    RosterStore getRosterStore() {
923        return rosterStore;
924    }
925
926    /**
927     * Returns the key to use in the presenceMap and entries Map for a fully qualified XMPP ID.
928     * The roster can contain any valid address format such us "domain/resource",
929     * "user@domain" or "user@domain/resource". If the roster contains an entry
930     * associated with the fully qualified XMPP ID then use the fully qualified XMPP
931     * ID as the key in presenceMap, otherwise use the bare address. Note: When the
932     * key in presenceMap is a fully qualified XMPP ID, the userPresences is useless
933     * since it will always contain one entry for the user.
934     *
935     * @param user the bare or fully qualified XMPP ID, e.g. jdoe@example.com or
936     *             jdoe@example.com/Work.
937     * @return the key to use in the presenceMap and entries Map for the fully qualified XMPP ID.
938     */
939    private String getMapKey(String user) {
940        if (user == null) {
941            return null;
942        }
943        if (entries.containsKey(user)) {
944            return user;
945        }
946        String key = XmppStringUtils.parseBareJid(user);
947        return key.toLowerCase(Locale.US);
948    }
949
950    /**
951     * Changes the presence of available contacts offline by simulating an unavailable
952     * presence sent from the server. After a disconnection, every Presence is set
953     * to offline.
954     * @throws NotConnectedException 
955     */
956    private void setOfflinePresencesAndResetLoaded() {
957        Presence packetUnavailable;
958        for (String user : presenceMap.keySet()) {
959            Map<String, Presence> resources = presenceMap.get(user);
960            if (resources != null) {
961                for (String resource : resources.keySet()) {
962                    packetUnavailable = new Presence(Presence.Type.unavailable);
963                    packetUnavailable.setFrom(user + "/" + resource);
964                    try {
965                        presencePacketListener.processPacket(packetUnavailable);
966                    }
967                    catch (NotConnectedException e) {
968                        throw new IllegalStateException(
969                                        "presencePakcetListener should never throw a NotConnectedException when processPacket is called with a presence of type unavailable",
970                                        e);
971                    }
972                }
973            }
974        }
975        rosterState = RosterState.uninitialized;
976    }
977
978    /**
979     * Fires roster changed event to roster listeners indicating that the
980     * specified collections of contacts have been added, updated or deleted
981     * from the roster.
982     *
983     * @param addedEntries   the collection of address of the added contacts.
984     * @param updatedEntries the collection of address of the updated contacts.
985     * @param deletedEntries the collection of address of the deleted contacts.
986     */
987    private void fireRosterChangedEvent(final Collection<String> addedEntries, final Collection<String> updatedEntries,
988                    final Collection<String> deletedEntries) {
989        synchronized (rosterListenersAndEntriesLock) {
990            for (RosterListener listener : rosterListeners) {
991                if (!addedEntries.isEmpty()) {
992                    listener.entriesAdded(addedEntries);
993                }
994                if (!updatedEntries.isEmpty()) {
995                    listener.entriesUpdated(updatedEntries);
996                }
997                if (!deletedEntries.isEmpty()) {
998                    listener.entriesDeleted(deletedEntries);
999                }
1000            }
1001        }
1002    }
1003
1004    /**
1005     * Fires roster presence changed event to roster listeners.
1006     *
1007     * @param presence the presence change.
1008     */
1009    private void fireRosterPresenceEvent(final Presence presence) {
1010        synchronized (rosterListenersAndEntriesLock) {
1011            for (RosterListener listener : rosterListeners) {
1012                listener.presenceChanged(presence);
1013            }
1014        }
1015    }
1016
1017    private void addUpdateEntry(Collection<String> addedEntries, Collection<String> updatedEntries,
1018                    Collection<String> unchangedEntries, RosterPacket.Item item, RosterEntry entry) {
1019        RosterEntry oldEntry;
1020        synchronized (rosterListenersAndEntriesLock) {
1021            oldEntry = entries.put(item.getUser(), entry);
1022        }
1023        if (oldEntry == null) {
1024            addedEntries.add(item.getUser());
1025        }
1026        else {
1027            RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry);
1028            if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) {
1029                updatedEntries.add(item.getUser());
1030            } else {
1031                // Record the entry as unchanged, so that it doesn't end up as deleted entry
1032                unchangedEntries.add(item.getUser());
1033            }
1034        }
1035
1036        // Mark the entry as unfiled if it does not belong to any groups.
1037        if (item.getGroupNames().isEmpty()) {
1038            unfiledEntries.add(entry);
1039        }
1040        else {
1041            unfiledEntries.remove(entry);
1042        }
1043
1044        // Add the entry/user to the groups
1045        List<String> newGroupNames = new ArrayList<String>();
1046        for (String groupName : item.getGroupNames()) {
1047            // Add the group name to the list.
1048            newGroupNames.add(groupName);
1049
1050            // Add the entry to the group.
1051            RosterGroup group = getGroup(groupName);
1052            if (group == null) {
1053                group = createGroup(groupName);
1054                groups.put(groupName, group);
1055            }
1056            // Add the entry.
1057            group.addEntryLocal(entry);
1058        }
1059
1060        // Remove user from the remaining groups.
1061        List<String> oldGroupNames = new ArrayList<String>();
1062        for (RosterGroup group: getGroups()) {
1063            oldGroupNames.add(group.getName());
1064        }
1065        oldGroupNames.removeAll(newGroupNames);
1066
1067        for (String groupName : oldGroupNames) {
1068            RosterGroup group = getGroup(groupName);
1069            group.removeEntryLocal(entry);
1070            if (group.getEntryCount() == 0) {
1071                groups.remove(groupName);
1072            }
1073        }
1074    }
1075
1076    private void deleteEntry(Collection<String> deletedEntries, RosterEntry entry) {
1077        String user = entry.getUser();
1078        entries.remove(user);
1079        unfiledEntries.remove(entry);
1080        presenceMap.remove(XmppStringUtils.parseBareJid(user));
1081        deletedEntries.add(user);
1082
1083        for (Entry<String,RosterGroup> e: groups.entrySet()) {
1084            RosterGroup group = e.getValue();
1085            group.removeEntryLocal(entry);
1086            if (group.getEntryCount() == 0) {
1087                groups.remove(e.getKey());
1088            }
1089        }
1090    }
1091
1092
1093    /**
1094     * Removes all the groups with no entries.
1095     *
1096     * This is used by {@link RosterPushListener} and {@link RosterResultListener} to
1097     * cleanup groups after removing contacts.
1098     */
1099    private void removeEmptyGroups() {
1100        // We have to do this because RosterGroup.removeEntry removes the entry immediately
1101        // (locally) and the group could remain empty.
1102        // TODO Check the performance/logic for rosters with large number of groups
1103        for (RosterGroup group : getGroups()) {
1104            if (group.getEntryCount() == 0) {
1105                groups.remove(group.getName());
1106            }
1107        }
1108    }
1109
1110    /**
1111     * Ignore ItemTypes as of RFC 6121, 2.1.2.5.
1112     *
1113     * This is used by {@link RosterPushListener} and {@link RosterResultListener}.
1114     * */
1115    private static boolean hasValidSubscriptionType(RosterPacket.Item item) {
1116        switch (item.getItemType()) {
1117            case none:
1118            case from:
1119            case to:
1120            case both:
1121                return true;
1122            default:
1123                return false;
1124        }
1125    }
1126
1127    /**
1128     * Check if the server supports roster versioning.
1129     *
1130     * @return true if the server supports roster versioning, false otherwise.
1131     */
1132    public boolean isRosterVersioningSupported() {
1133        return connection().hasFeature(RosterVer.ELEMENT, RosterVer.NAMESPACE);
1134    }
1135
1136    /**
1137     * An enumeration for the subscription mode options.
1138     */
1139    public enum SubscriptionMode {
1140
1141        /**
1142         * Automatically accept all subscription and unsubscription requests. This is
1143         * the default mode and is suitable for simple client. More complex client will
1144         * likely wish to handle subscription requests manually.
1145         */
1146        accept_all,
1147
1148        /**
1149         * Automatically reject all subscription requests.
1150         */
1151        reject_all,
1152
1153        /**
1154         * Subscription requests are ignored, which means they must be manually
1155         * processed by registering a listener for presence packets and then looking
1156         * for any presence requests that have the type Presence.Type.SUBSCRIBE or
1157         * Presence.Type.UNSUBSCRIBE.
1158         */
1159        manual
1160    }
1161
1162    /**
1163     * Listens for all presence packets and processes them.
1164     */
1165    private class PresencePacketListener implements StanzaListener {
1166
1167        /**
1168         * Retrieve the user presences (a map from resource to {@link Presence}) for a given key (usually a JID without
1169         * a resource). If the {@link #presenceMap} does not contain already a user presence map, then it will be
1170         * created.
1171         * 
1172         * @param key the presence map key
1173         * @return the user presences
1174         */
1175        private Map<String, Presence> getUserPresences(String key) {
1176            Map<String, Presence> userPresences = presenceMap.get(key);
1177            if (userPresences == null) {
1178                userPresences = new ConcurrentHashMap<>();
1179                presenceMap.put(key, userPresences);
1180            }
1181            return userPresences;
1182        }
1183
1184        @Override
1185        public void processPacket(Stanza packet) throws NotConnectedException {
1186            // Try to ensure that the roster is loaded when processing presence stanzas. While the
1187            // presence listener is synchronous, the roster result listener is not, which means that
1188            // the presence listener may be invoked with a not yet loaded roster.
1189            if (rosterState == RosterState.loading) {
1190                try {
1191                    waitUntilLoaded();
1192                }
1193                catch (InterruptedException e) {
1194                    LOGGER.log(Level.INFO, "Presence listener was interrupted", e);
1195
1196                }
1197            }
1198            if (!isLoaded() && rosterLoadedAtLogin) {
1199                LOGGER.warning("Roster not loaded while processing presence stanza");
1200            }
1201            final XMPPConnection connection = connection();
1202            Presence presence = (Presence) packet;
1203            String from = presence.getFrom();
1204            String key = getMapKey(from);
1205            Map<String, Presence> userPresences;
1206            Presence response = null;
1207
1208            // If an "available" presence, add it to the presence map. Each presence
1209            // map will hold for a particular user a map with the presence
1210            // packets saved for each resource.
1211            switch (presence.getType()) {
1212            case available:
1213                // Get the user presence map
1214                userPresences = getUserPresences(key);
1215                // See if an offline presence was being stored in the map. If so, remove
1216                // it since we now have an online presence.
1217                userPresences.remove("");
1218                // Add the new presence, using the resources as a key.
1219                userPresences.put(XmppStringUtils.parseResource(from), presence);
1220                // If the user is in the roster, fire an event.
1221                if (entries.containsKey(key)) {
1222                    fireRosterPresenceEvent(presence);
1223                }
1224                break;
1225            // If an "unavailable" packet.
1226            case unavailable:
1227                // If no resource, this is likely an offline presence as part of
1228                // a roster presence flood. In that case, we store it.
1229                if ("".equals(XmppStringUtils.parseResource(from))) {
1230                    // Get the user presence map
1231                    userPresences = getUserPresences(key);
1232                    userPresences.put("", presence);
1233                }
1234                // Otherwise, this is a normal offline presence.
1235                else if (presenceMap.get(key) != null) {
1236                    userPresences = presenceMap.get(key);
1237                    // Store the offline presence, as it may include extra information
1238                    // such as the user being on vacation.
1239                    userPresences.put(XmppStringUtils.parseResource(from), presence);
1240                }
1241                // If the user is in the roster, fire an event.
1242                if (entries.containsKey(key)) {
1243                    fireRosterPresenceEvent(presence);
1244                }
1245                break;
1246            case subscribe:
1247                switch (subscriptionMode) {
1248                case accept_all:
1249                    // Accept all subscription requests.
1250                    response = new Presence(Presence.Type.subscribed);
1251                    break;
1252                case reject_all:
1253                    // Reject all subscription requests.
1254                    response = new Presence(Presence.Type.unsubscribed);
1255                    break;
1256                case manual:
1257                default:
1258                    // Otherwise, in manual mode so ignore.
1259                    break;
1260                }
1261                if (response != null) {
1262                    response.setTo(presence.getFrom());
1263                    connection.sendStanza(response);
1264                }
1265                break;
1266            case unsubscribe:
1267                if (subscriptionMode != SubscriptionMode.manual) {
1268                    // Acknowledge and accept unsubscription notification so that the
1269                    // server will stop sending notifications saying that the contact
1270                    // has unsubscribed to our presence.
1271                    response = new Presence(Presence.Type.unsubscribed);
1272                    response.setTo(presence.getFrom());
1273                    connection.sendStanza(response);
1274                }
1275                // Otherwise, in manual mode so ignore.
1276                break;
1277            // Error presence packets from a bare JID mean we invalidate all existing
1278            // presence info for the user.
1279            case error:
1280                if (!"".equals(XmppStringUtils.parseResource(from))) {
1281                    break;
1282                }
1283                userPresences = getUserPresences(key);
1284                // Any other presence data is invalidated by the error packet.
1285                userPresences.clear();
1286
1287                // Set the new presence using the empty resource as a key.
1288                userPresences.put("", presence);
1289                // If the user is in the roster, fire an event.
1290                if (entries.containsKey(key)) {
1291                    fireRosterPresenceEvent(presence);
1292                }
1293                break;
1294            default:
1295                break;
1296            }
1297        }
1298    }
1299
1300    /**
1301     * Handles roster reults as described in RFC 6121 2.1.4
1302     */
1303    private class RosterResultListener implements StanzaListener {
1304
1305        @Override
1306        public void processPacket(Stanza packet) {
1307            final XMPPConnection connection = connection();
1308            LOGGER.fine("RosterResultListener received stanza");
1309            Collection<String> addedEntries = new ArrayList<String>();
1310            Collection<String> updatedEntries = new ArrayList<String>();
1311            Collection<String> deletedEntries = new ArrayList<String>();
1312            Collection<String> unchangedEntries = new ArrayList<String>();
1313
1314            if (packet instanceof RosterPacket) {
1315                // Non-empty roster result. This stanza contains all the roster elements.
1316                RosterPacket rosterPacket = (RosterPacket) packet;
1317
1318                // Ignore items without valid subscription type
1319                ArrayList<Item> validItems = new ArrayList<RosterPacket.Item>();
1320                for (RosterPacket.Item item : rosterPacket.getRosterItems()) {
1321                    if (hasValidSubscriptionType(item)) {
1322                        validItems.add(item);
1323                    }
1324                }
1325
1326                for (RosterPacket.Item item : validItems) {
1327                    RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
1328                            item.getItemType(), item.getItemStatus(), Roster.this, connection);
1329                    addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
1330                }
1331
1332                // Delete all entries which where not added or updated
1333                Set<String> toDelete = new HashSet<String>();
1334                for (RosterEntry entry : entries.values()) {
1335                    toDelete.add(entry.getUser());
1336                }
1337                toDelete.removeAll(addedEntries);
1338                toDelete.removeAll(updatedEntries);
1339                toDelete.removeAll(unchangedEntries);
1340                for (String user : toDelete) {
1341                    deleteEntry(deletedEntries, entries.get(user));
1342                }
1343
1344                if (rosterStore != null) {
1345                    String version = rosterPacket.getVersion();
1346                    rosterStore.resetEntries(validItems, version);
1347                }
1348
1349                removeEmptyGroups();
1350            }
1351            else {
1352                // Empty roster result as defined in RFC6121 2.6.3. An empty roster result basically
1353                // means that rosterver was used and the roster hasn't changed (much) since the
1354                // version we presented the server. So we simply load the roster from the store and
1355                // await possible further roster pushes.
1356                for (RosterPacket.Item item : rosterStore.getEntries()) {
1357                    RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
1358                            item.getItemType(), item.getItemStatus(), Roster.this, connection);
1359                    addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
1360                }
1361            }
1362
1363            rosterState = RosterState.loaded;
1364            synchronized (Roster.this) {
1365                Roster.this.notifyAll();
1366            }
1367            // Fire event for roster listeners.
1368            fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
1369
1370            // Call the roster loaded listeners after the roster events have been fired. This is
1371            // imporant because the user may call getEntriesAndAddListener() in onRosterLoaded(),
1372            // and if the order would be the other way around, the roster listener added by
1373            // getEntriesAndAddListener() would be invoked with information that was already
1374            // available at the time getEntriesAndAddListenr() was called.
1375            try {
1376                synchronized (rosterLoadedListeners) {
1377                    for (RosterLoadedListener rosterLoadedListener : rosterLoadedListeners) {
1378                        rosterLoadedListener.onRosterLoaded(Roster.this);
1379                    }
1380                }
1381            }
1382            catch (Exception e) {
1383                LOGGER.log(Level.WARNING, "RosterLoadedListener threw exception", e);
1384            }
1385        }
1386    }
1387
1388    /**
1389     * Listens for all roster pushes and processes them.
1390     */
1391    private class RosterPushListener extends AbstractIqRequestHandler {
1392
1393        private RosterPushListener() {
1394            super(RosterPacket.ELEMENT, RosterPacket.NAMESPACE, Type.set, Mode.sync);
1395        }
1396
1397        @Override
1398        public IQ handleIQRequest(IQ iqRequest) {
1399            final XMPPConnection connection = connection();
1400            RosterPacket rosterPacket = (RosterPacket) iqRequest;
1401
1402            // Roster push (RFC 6121, 2.1.6)
1403            // A roster push with a non-empty from not matching our address MUST be ignored
1404            String jid = XmppStringUtils.parseBareJid(connection.getUser());
1405            String from = rosterPacket.getFrom();
1406            if (from != null && !from.equals(jid)) {
1407                LOGGER.warning("Ignoring roster push with a non matching 'from' ourJid='" + jid + "' from='" + from
1408                                + "'");
1409                return IQ.createErrorResponse(iqRequest, new XMPPError(Condition.service_unavailable));
1410            }
1411
1412            // A roster push must contain exactly one entry
1413            Collection<Item> items = rosterPacket.getRosterItems();
1414            if (items.size() != 1) {
1415                LOGGER.warning("Ignoring roster push with not exaclty one entry. size=" + items.size());
1416                return IQ.createErrorResponse(iqRequest, new XMPPError(Condition.bad_request));
1417            }
1418
1419            Collection<String> addedEntries = new ArrayList<String>();
1420            Collection<String> updatedEntries = new ArrayList<String>();
1421            Collection<String> deletedEntries = new ArrayList<String>();
1422            Collection<String> unchangedEntries = new ArrayList<String>();
1423
1424            // We assured above that the size of items is exaclty 1, therefore we are able to
1425            // safely retrieve this single item here.
1426            Item item = items.iterator().next();
1427            RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
1428                            item.getItemType(), item.getItemStatus(), Roster.this, connection);
1429            String version = rosterPacket.getVersion();
1430
1431            if (item.getItemType().equals(RosterPacket.ItemType.remove)) {
1432                deleteEntry(deletedEntries, entry);
1433                if (rosterStore != null) {
1434                    rosterStore.removeEntry(entry.getUser(), version);
1435                }
1436            }
1437            else if (hasValidSubscriptionType(item)) {
1438                addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
1439                if (rosterStore != null) {
1440                    rosterStore.addEntry(item, version);
1441                }
1442            }
1443
1444            removeEmptyGroups();
1445
1446            // Fire event for roster listeners.
1447            fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
1448
1449            return IQ.createResultIQ(rosterPacket);
1450        }
1451    }
1452}