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 for newly created instances
901     * of {@link Roster}.
902     *
903     * @param rosterLoadedAtLoginDefault if the roster will be loaded from the server when logging in.
904     * @see #setRosterLoadedAtLogin(boolean)
905     * @since 4.1.7
906     */
907    public static void setRosterLoadedAtLoginDefault(boolean rosterLoadedAtLoginDefault) {
908        Roster.rosterLoadedAtLoginDefault = rosterLoadedAtLoginDefault;
909    }
910
911    /**
912     * Sets if the roster will be loaded from the server when logging in. This
913     * is the common behaviour for clients but sometimes clients may want to differ this
914     * or just never do it if not interested in rosters.
915     *
916     * @param rosterLoadedAtLogin if the roster will be loaded from the server when logging in.
917     */
918    public void setRosterLoadedAtLogin(boolean rosterLoadedAtLogin) {
919        this.rosterLoadedAtLogin = rosterLoadedAtLogin;
920    }
921
922    /**
923     * Returns true if the roster will be loaded from the server when logging in. This
924     * is the common behavior for clients but sometimes clients may want to differ this
925     * or just never do it if not interested in rosters.
926     *
927     * @return true if the roster will be loaded from the server when logging in.
928     * @see <a href="http://xmpp.org/rfcs/rfc6121.html#roster-login">RFC 6121 2.2 - Retrieving the Roster on Login</a>
929     */
930    public boolean isRosterLoadedAtLogin() {
931        return rosterLoadedAtLogin;
932    }
933
934    RosterStore getRosterStore() {
935        return rosterStore;
936    }
937
938    /**
939     * Returns the key to use in the presenceMap and entries Map for a fully qualified XMPP ID.
940     * The roster can contain any valid address format such us "domain/resource",
941     * "user@domain" or "user@domain/resource". If the roster contains an entry
942     * associated with the fully qualified XMPP ID then use the fully qualified XMPP
943     * ID as the key in presenceMap, otherwise use the bare address. Note: When the
944     * key in presenceMap is a fully qualified XMPP ID, the userPresences is useless
945     * since it will always contain one entry for the user.
946     *
947     * @param user the bare or fully qualified XMPP ID, e.g. jdoe@example.com or
948     *             jdoe@example.com/Work.
949     * @return the key to use in the presenceMap and entries Map for the fully qualified XMPP ID.
950     */
951    private String getMapKey(String user) {
952        if (user == null) {
953            return null;
954        }
955        if (entries.containsKey(user)) {
956            return user;
957        }
958        String key = XmppStringUtils.parseBareJid(user);
959        return key.toLowerCase(Locale.US);
960    }
961
962    /**
963     * Changes the presence of available contacts offline by simulating an unavailable
964     * presence sent from the server. After a disconnection, every Presence is set
965     * to offline.
966     * @throws NotConnectedException 
967     */
968    private void setOfflinePresencesAndResetLoaded() {
969        Presence packetUnavailable;
970        for (String user : presenceMap.keySet()) {
971            Map<String, Presence> resources = presenceMap.get(user);
972            if (resources != null) {
973                for (String resource : resources.keySet()) {
974                    packetUnavailable = new Presence(Presence.Type.unavailable);
975                    packetUnavailable.setFrom(user + "/" + resource);
976                    try {
977                        presencePacketListener.processPacket(packetUnavailable);
978                    }
979                    catch (NotConnectedException e) {
980                        throw new IllegalStateException(
981                                        "presencePakcetListener should never throw a NotConnectedException when processPacket is called with a presence of type unavailable",
982                                        e);
983                    }
984                }
985            }
986        }
987        rosterState = RosterState.uninitialized;
988    }
989
990    /**
991     * Fires roster changed event to roster listeners indicating that the
992     * specified collections of contacts have been added, updated or deleted
993     * from the roster.
994     *
995     * @param addedEntries   the collection of address of the added contacts.
996     * @param updatedEntries the collection of address of the updated contacts.
997     * @param deletedEntries the collection of address of the deleted contacts.
998     */
999    private void fireRosterChangedEvent(final Collection<String> addedEntries, final Collection<String> updatedEntries,
1000                    final Collection<String> deletedEntries) {
1001        synchronized (rosterListenersAndEntriesLock) {
1002            for (RosterListener listener : rosterListeners) {
1003                if (!addedEntries.isEmpty()) {
1004                    listener.entriesAdded(addedEntries);
1005                }
1006                if (!updatedEntries.isEmpty()) {
1007                    listener.entriesUpdated(updatedEntries);
1008                }
1009                if (!deletedEntries.isEmpty()) {
1010                    listener.entriesDeleted(deletedEntries);
1011                }
1012            }
1013        }
1014    }
1015
1016    /**
1017     * Fires roster presence changed event to roster listeners.
1018     *
1019     * @param presence the presence change.
1020     */
1021    private void fireRosterPresenceEvent(final Presence presence) {
1022        synchronized (rosterListenersAndEntriesLock) {
1023            for (RosterListener listener : rosterListeners) {
1024                listener.presenceChanged(presence);
1025            }
1026        }
1027    }
1028
1029    private void addUpdateEntry(Collection<String> addedEntries, Collection<String> updatedEntries,
1030                    Collection<String> unchangedEntries, RosterPacket.Item item, RosterEntry entry) {
1031        RosterEntry oldEntry;
1032        synchronized (rosterListenersAndEntriesLock) {
1033            oldEntry = entries.put(item.getUser(), entry);
1034        }
1035        if (oldEntry == null) {
1036            addedEntries.add(item.getUser());
1037        }
1038        else {
1039            RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry);
1040            if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) {
1041                updatedEntries.add(item.getUser());
1042            } else {
1043                // Record the entry as unchanged, so that it doesn't end up as deleted entry
1044                unchangedEntries.add(item.getUser());
1045            }
1046        }
1047
1048        // Mark the entry as unfiled if it does not belong to any groups.
1049        if (item.getGroupNames().isEmpty()) {
1050            unfiledEntries.add(entry);
1051        }
1052        else {
1053            unfiledEntries.remove(entry);
1054        }
1055
1056        // Add the entry/user to the groups
1057        List<String> newGroupNames = new ArrayList<String>();
1058        for (String groupName : item.getGroupNames()) {
1059            // Add the group name to the list.
1060            newGroupNames.add(groupName);
1061
1062            // Add the entry to the group.
1063            RosterGroup group = getGroup(groupName);
1064            if (group == null) {
1065                group = createGroup(groupName);
1066                groups.put(groupName, group);
1067            }
1068            // Add the entry.
1069            group.addEntryLocal(entry);
1070        }
1071
1072        // Remove user from the remaining groups.
1073        List<String> oldGroupNames = new ArrayList<String>();
1074        for (RosterGroup group: getGroups()) {
1075            oldGroupNames.add(group.getName());
1076        }
1077        oldGroupNames.removeAll(newGroupNames);
1078
1079        for (String groupName : oldGroupNames) {
1080            RosterGroup group = getGroup(groupName);
1081            group.removeEntryLocal(entry);
1082            if (group.getEntryCount() == 0) {
1083                groups.remove(groupName);
1084            }
1085        }
1086    }
1087
1088    private void deleteEntry(Collection<String> deletedEntries, RosterEntry entry) {
1089        String user = entry.getUser();
1090        entries.remove(user);
1091        unfiledEntries.remove(entry);
1092        presenceMap.remove(XmppStringUtils.parseBareJid(user));
1093        deletedEntries.add(user);
1094
1095        for (Entry<String,RosterGroup> e: groups.entrySet()) {
1096            RosterGroup group = e.getValue();
1097            group.removeEntryLocal(entry);
1098            if (group.getEntryCount() == 0) {
1099                groups.remove(e.getKey());
1100            }
1101        }
1102    }
1103
1104
1105    /**
1106     * Removes all the groups with no entries.
1107     *
1108     * This is used by {@link RosterPushListener} and {@link RosterResultListener} to
1109     * cleanup groups after removing contacts.
1110     */
1111    private void removeEmptyGroups() {
1112        // We have to do this because RosterGroup.removeEntry removes the entry immediately
1113        // (locally) and the group could remain empty.
1114        // TODO Check the performance/logic for rosters with large number of groups
1115        for (RosterGroup group : getGroups()) {
1116            if (group.getEntryCount() == 0) {
1117                groups.remove(group.getName());
1118            }
1119        }
1120    }
1121
1122    /**
1123     * Ignore ItemTypes as of RFC 6121, 2.1.2.5.
1124     *
1125     * This is used by {@link RosterPushListener} and {@link RosterResultListener}.
1126     * */
1127    private static boolean hasValidSubscriptionType(RosterPacket.Item item) {
1128        switch (item.getItemType()) {
1129            case none:
1130            case from:
1131            case to:
1132            case both:
1133                return true;
1134            default:
1135                return false;
1136        }
1137    }
1138
1139    /**
1140     * Check if the server supports roster versioning.
1141     *
1142     * @return true if the server supports roster versioning, false otherwise.
1143     */
1144    public boolean isRosterVersioningSupported() {
1145        return connection().hasFeature(RosterVer.ELEMENT, RosterVer.NAMESPACE);
1146    }
1147
1148    /**
1149     * An enumeration for the subscription mode options.
1150     */
1151    public enum SubscriptionMode {
1152
1153        /**
1154         * Automatically accept all subscription and unsubscription requests. This is
1155         * the default mode and is suitable for simple client. More complex client will
1156         * likely wish to handle subscription requests manually.
1157         */
1158        accept_all,
1159
1160        /**
1161         * Automatically reject all subscription requests.
1162         */
1163        reject_all,
1164
1165        /**
1166         * Subscription requests are ignored, which means they must be manually
1167         * processed by registering a listener for presence packets and then looking
1168         * for any presence requests that have the type Presence.Type.SUBSCRIBE or
1169         * Presence.Type.UNSUBSCRIBE.
1170         */
1171        manual
1172    }
1173
1174    /**
1175     * Listens for all presence packets and processes them.
1176     */
1177    private class PresencePacketListener implements StanzaListener {
1178
1179        /**
1180         * Retrieve the user presences (a map from resource to {@link Presence}) for a given key (usually a JID without
1181         * a resource). If the {@link #presenceMap} does not contain already a user presence map, then it will be
1182         * created.
1183         * 
1184         * @param key the presence map key
1185         * @return the user presences
1186         */
1187        private Map<String, Presence> getUserPresences(String key) {
1188            Map<String, Presence> userPresences = presenceMap.get(key);
1189            if (userPresences == null) {
1190                userPresences = new ConcurrentHashMap<>();
1191                presenceMap.put(key, userPresences);
1192            }
1193            return userPresences;
1194        }
1195
1196        @Override
1197        public void processPacket(Stanza packet) throws NotConnectedException {
1198            // Try to ensure that the roster is loaded when processing presence stanzas. While the
1199            // presence listener is synchronous, the roster result listener is not, which means that
1200            // the presence listener may be invoked with a not yet loaded roster.
1201            if (rosterState == RosterState.loading) {
1202                try {
1203                    waitUntilLoaded();
1204                }
1205                catch (InterruptedException e) {
1206                    LOGGER.log(Level.INFO, "Presence listener was interrupted", e);
1207
1208                }
1209            }
1210            if (!isLoaded() && rosterLoadedAtLogin) {
1211                LOGGER.warning("Roster not loaded while processing presence stanza");
1212            }
1213            final XMPPConnection connection = connection();
1214            Presence presence = (Presence) packet;
1215            String from = presence.getFrom();
1216            String key = getMapKey(from);
1217            Map<String, Presence> userPresences;
1218            Presence response = null;
1219
1220            // If an "available" presence, add it to the presence map. Each presence
1221            // map will hold for a particular user a map with the presence
1222            // packets saved for each resource.
1223            switch (presence.getType()) {
1224            case available:
1225                // Get the user presence map
1226                userPresences = getUserPresences(key);
1227                // See if an offline presence was being stored in the map. If so, remove
1228                // it since we now have an online presence.
1229                userPresences.remove("");
1230                // Add the new presence, using the resources as a key.
1231                userPresences.put(XmppStringUtils.parseResource(from), presence);
1232                // If the user is in the roster, fire an event.
1233                if (entries.containsKey(key)) {
1234                    fireRosterPresenceEvent(presence);
1235                }
1236                break;
1237            // If an "unavailable" packet.
1238            case unavailable:
1239                // If no resource, this is likely an offline presence as part of
1240                // a roster presence flood. In that case, we store it.
1241                if ("".equals(XmppStringUtils.parseResource(from))) {
1242                    // Get the user presence map
1243                    userPresences = getUserPresences(key);
1244                    userPresences.put("", presence);
1245                }
1246                // Otherwise, this is a normal offline presence.
1247                else if (presenceMap.get(key) != null) {
1248                    userPresences = presenceMap.get(key);
1249                    // Store the offline presence, as it may include extra information
1250                    // such as the user being on vacation.
1251                    userPresences.put(XmppStringUtils.parseResource(from), presence);
1252                }
1253                // If the user is in the roster, fire an event.
1254                if (entries.containsKey(key)) {
1255                    fireRosterPresenceEvent(presence);
1256                }
1257                break;
1258            case subscribe:
1259                switch (subscriptionMode) {
1260                case accept_all:
1261                    // Accept all subscription requests.
1262                    response = new Presence(Presence.Type.subscribed);
1263                    break;
1264                case reject_all:
1265                    // Reject all subscription requests.
1266                    response = new Presence(Presence.Type.unsubscribed);
1267                    break;
1268                case manual:
1269                default:
1270                    // Otherwise, in manual mode so ignore.
1271                    break;
1272                }
1273                if (response != null) {
1274                    response.setTo(presence.getFrom());
1275                    connection.sendStanza(response);
1276                }
1277                break;
1278            case unsubscribe:
1279                if (subscriptionMode != SubscriptionMode.manual) {
1280                    // Acknowledge and accept unsubscription notification so that the
1281                    // server will stop sending notifications saying that the contact
1282                    // has unsubscribed to our presence.
1283                    response = new Presence(Presence.Type.unsubscribed);
1284                    response.setTo(presence.getFrom());
1285                    connection.sendStanza(response);
1286                }
1287                // Otherwise, in manual mode so ignore.
1288                break;
1289            // Error presence packets from a bare JID mean we invalidate all existing
1290            // presence info for the user.
1291            case error:
1292                if (!"".equals(XmppStringUtils.parseResource(from))) {
1293                    break;
1294                }
1295                userPresences = getUserPresences(key);
1296                // Any other presence data is invalidated by the error packet.
1297                userPresences.clear();
1298
1299                // Set the new presence using the empty resource as a key.
1300                userPresences.put("", presence);
1301                // If the user is in the roster, fire an event.
1302                if (entries.containsKey(key)) {
1303                    fireRosterPresenceEvent(presence);
1304                }
1305                break;
1306            default:
1307                break;
1308            }
1309        }
1310    }
1311
1312    /**
1313     * Handles roster reults as described in RFC 6121 2.1.4
1314     */
1315    private class RosterResultListener implements StanzaListener {
1316
1317        @Override
1318        public void processPacket(Stanza packet) {
1319            final XMPPConnection connection = connection();
1320            LOGGER.fine("RosterResultListener received stanza");
1321            Collection<String> addedEntries = new ArrayList<String>();
1322            Collection<String> updatedEntries = new ArrayList<String>();
1323            Collection<String> deletedEntries = new ArrayList<String>();
1324            Collection<String> unchangedEntries = new ArrayList<String>();
1325
1326            if (packet instanceof RosterPacket) {
1327                // Non-empty roster result. This stanza contains all the roster elements.
1328                RosterPacket rosterPacket = (RosterPacket) packet;
1329
1330                // Ignore items without valid subscription type
1331                ArrayList<Item> validItems = new ArrayList<RosterPacket.Item>();
1332                for (RosterPacket.Item item : rosterPacket.getRosterItems()) {
1333                    if (hasValidSubscriptionType(item)) {
1334                        validItems.add(item);
1335                    }
1336                }
1337
1338                for (RosterPacket.Item item : validItems) {
1339                    RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
1340                            item.getItemType(), item.getItemStatus(), Roster.this, connection);
1341                    addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
1342                }
1343
1344                // Delete all entries which where not added or updated
1345                Set<String> toDelete = new HashSet<String>();
1346                for (RosterEntry entry : entries.values()) {
1347                    toDelete.add(entry.getUser());
1348                }
1349                toDelete.removeAll(addedEntries);
1350                toDelete.removeAll(updatedEntries);
1351                toDelete.removeAll(unchangedEntries);
1352                for (String user : toDelete) {
1353                    deleteEntry(deletedEntries, entries.get(user));
1354                }
1355
1356                if (rosterStore != null) {
1357                    String version = rosterPacket.getVersion();
1358                    rosterStore.resetEntries(validItems, version);
1359                }
1360
1361                removeEmptyGroups();
1362            }
1363            else {
1364                // Empty roster result as defined in RFC6121 2.6.3. An empty roster result basically
1365                // means that rosterver was used and the roster hasn't changed (much) since the
1366                // version we presented the server. So we simply load the roster from the store and
1367                // await possible further roster pushes.
1368                for (RosterPacket.Item item : rosterStore.getEntries()) {
1369                    RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
1370                            item.getItemType(), item.getItemStatus(), Roster.this, connection);
1371                    addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
1372                }
1373            }
1374
1375            rosterState = RosterState.loaded;
1376            synchronized (Roster.this) {
1377                Roster.this.notifyAll();
1378            }
1379            // Fire event for roster listeners.
1380            fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
1381
1382            // Call the roster loaded listeners after the roster events have been fired. This is
1383            // imporant because the user may call getEntriesAndAddListener() in onRosterLoaded(),
1384            // and if the order would be the other way around, the roster listener added by
1385            // getEntriesAndAddListener() would be invoked with information that was already
1386            // available at the time getEntriesAndAddListenr() was called.
1387            try {
1388                synchronized (rosterLoadedListeners) {
1389                    for (RosterLoadedListener rosterLoadedListener : rosterLoadedListeners) {
1390                        rosterLoadedListener.onRosterLoaded(Roster.this);
1391                    }
1392                }
1393            }
1394            catch (Exception e) {
1395                LOGGER.log(Level.WARNING, "RosterLoadedListener threw exception", e);
1396            }
1397        }
1398    }
1399
1400    /**
1401     * Listens for all roster pushes and processes them.
1402     */
1403    private class RosterPushListener extends AbstractIqRequestHandler {
1404
1405        private RosterPushListener() {
1406            super(RosterPacket.ELEMENT, RosterPacket.NAMESPACE, Type.set, Mode.sync);
1407        }
1408
1409        @Override
1410        public IQ handleIQRequest(IQ iqRequest) {
1411            final XMPPConnection connection = connection();
1412            RosterPacket rosterPacket = (RosterPacket) iqRequest;
1413
1414            // Roster push (RFC 6121, 2.1.6)
1415            // A roster push with a non-empty from not matching our address MUST be ignored
1416            String jid = XmppStringUtils.parseBareJid(connection.getUser());
1417            String from = rosterPacket.getFrom();
1418            if (from != null && !from.equals(jid)) {
1419                LOGGER.warning("Ignoring roster push with a non matching 'from' ourJid='" + jid + "' from='" + from
1420                                + "'");
1421                return IQ.createErrorResponse(iqRequest, new XMPPError(Condition.service_unavailable));
1422            }
1423
1424            // A roster push must contain exactly one entry
1425            Collection<Item> items = rosterPacket.getRosterItems();
1426            if (items.size() != 1) {
1427                LOGGER.warning("Ignoring roster push with not exaclty one entry. size=" + items.size());
1428                return IQ.createErrorResponse(iqRequest, new XMPPError(Condition.bad_request));
1429            }
1430
1431            Collection<String> addedEntries = new ArrayList<String>();
1432            Collection<String> updatedEntries = new ArrayList<String>();
1433            Collection<String> deletedEntries = new ArrayList<String>();
1434            Collection<String> unchangedEntries = new ArrayList<String>();
1435
1436            // We assured above that the size of items is exaclty 1, therefore we are able to
1437            // safely retrieve this single item here.
1438            Item item = items.iterator().next();
1439            RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
1440                            item.getItemType(), item.getItemStatus(), Roster.this, connection);
1441            String version = rosterPacket.getVersion();
1442
1443            if (item.getItemType().equals(RosterPacket.ItemType.remove)) {
1444                deleteEntry(deletedEntries, entry);
1445                if (rosterStore != null) {
1446                    rosterStore.removeEntry(entry.getUser(), version);
1447                }
1448            }
1449            else if (hasValidSubscriptionType(item)) {
1450                addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
1451                if (rosterStore != null) {
1452                    rosterStore.addEntry(item, version);
1453                }
1454            }
1455
1456            removeEmptyGroups();
1457
1458            // Fire event for roster listeners.
1459            fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
1460
1461            return IQ.createResultIQ(rosterPacket);
1462        }
1463    }
1464}