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;
019
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Map.Entry;
029import java.util.Set;
030import java.util.concurrent.ConcurrentHashMap;
031import java.util.concurrent.CopyOnWriteArrayList;
032import java.util.logging.Level;
033import java.util.logging.Logger;
034
035import org.jivesoftware.smack.SmackException.NoResponseException;
036import org.jivesoftware.smack.SmackException.NotConnectedException;
037import org.jivesoftware.smack.SmackException.NotLoggedInException;
038import org.jivesoftware.smack.XMPPException.XMPPErrorException;
039import org.jivesoftware.smack.filter.AndFilter;
040import org.jivesoftware.smack.filter.IQReplyFilter;
041import org.jivesoftware.smack.filter.IQTypeFilter;
042import org.jivesoftware.smack.filter.PacketFilter;
043import org.jivesoftware.smack.filter.PacketTypeFilter;
044import org.jivesoftware.smack.packet.IQ;
045import org.jivesoftware.smack.packet.Packet;
046import org.jivesoftware.smack.packet.Presence;
047import org.jivesoftware.smack.packet.RosterPacket;
048import org.jivesoftware.smack.packet.RosterPacket.Item;
049import org.jivesoftware.smack.util.StringUtils;
050
051/**
052 * Represents a user's roster, which is the collection of users a person receives
053 * presence updates for. Roster items are categorized into groups for easier management.<p>
054 * <p/>
055 * Others users may attempt to subscribe to this user using a subscription request. Three
056 * modes are supported for handling these requests: <ul>
057 * <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li>
058 * <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li>
059 * <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li>
060 * </ul>
061 *
062 * @author Matt Tucker
063 * @see XMPPConnection#getRoster()
064 */
065public class Roster {
066
067    private static final Logger LOGGER = Logger.getLogger(Roster.class.getName());
068
069    private static final PacketFilter ROSTER_PUSH_FILTER = new AndFilter(new PacketTypeFilter(
070                    RosterPacket.class), new IQTypeFilter(IQ.Type.SET));
071
072    private static final PacketFilter PRESENCE_PACKET_FILTER = new PacketTypeFilter(Presence.class);
073
074    /**
075     * The default subscription processing mode to use when a Roster is created. By default
076     * all subscription requests are automatically accepted.
077     */
078    private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.accept_all;
079
080    private final XMPPConnection connection;
081    private final RosterStore rosterStore;
082    private final Map<String, RosterGroup> groups = new ConcurrentHashMap<String, RosterGroup>();
083    private final Map<String,RosterEntry> entries = new ConcurrentHashMap<String,RosterEntry>();
084    private final List<RosterEntry> unfiledEntries = new CopyOnWriteArrayList<RosterEntry>();
085    private final List<RosterListener> rosterListeners = new CopyOnWriteArrayList<RosterListener>();
086    private final Map<String, Map<String, Presence>> presenceMap = new ConcurrentHashMap<String, Map<String, Presence>>();
087    // The roster is marked as initialized when at least a single roster packet
088    // has been received and processed.
089    boolean rosterInitialized = false;
090    private final PresencePacketListener presencePacketListener = new PresencePacketListener();
091
092    private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode();
093
094    /**
095     * Returns the default subscription processing mode to use when a new Roster is created. The
096     * subscription processing mode dictates what action Smack will take when subscription
097     * requests from other users are made. The default subscription mode
098     * is {@link SubscriptionMode#accept_all}.
099     *
100     * @return the default subscription mode to use for new Rosters
101     */
102    public static SubscriptionMode getDefaultSubscriptionMode() {
103        return defaultSubscriptionMode;
104    }
105
106    /**
107     * Sets the default subscription processing mode to use when a new Roster is created. The
108     * subscription processing mode dictates what action Smack will take when subscription
109     * requests from other users are made. The default subscription mode
110     * is {@link SubscriptionMode#accept_all}.
111     *
112     * @param subscriptionMode the default subscription mode to use for new Rosters.
113     */
114    public static void setDefaultSubscriptionMode(SubscriptionMode subscriptionMode) {
115        defaultSubscriptionMode = subscriptionMode;
116    }
117
118    /**
119     * Creates a new roster.
120     *
121     * @param connection an XMPP connection.
122     */
123    Roster(final XMPPConnection connection) {
124        this.connection = connection;
125        rosterStore = connection.getConfiguration().getRosterStore();
126        // Listen for any roster packets.
127        connection.addPacketListener(new RosterPushListener(), ROSTER_PUSH_FILTER);
128        // Listen for any presence packets.
129        connection.addPacketListener(presencePacketListener, PRESENCE_PACKET_FILTER);
130
131        // Listen for connection events
132        connection.addConnectionListener(new AbstractConnectionListener() {
133            
134            public void connectionClosed() {
135                // Changes the presence available contacts to unavailable
136                try {
137                    setOfflinePresences();
138                }
139                catch (NotConnectedException e) {
140                    LOGGER.log(Level.SEVERE, "Not connected exception" ,e);
141                }
142            }
143
144            public void connectionClosedOnError(Exception e) {
145                // Changes the presence available contacts to unavailable
146                try {
147                    setOfflinePresences();
148                }
149                catch (NotConnectedException e1) {
150                    LOGGER.log(Level.SEVERE, "Not connected exception" ,e);
151                }
152            }
153
154        });
155        // If the connection is already established, call reload
156        if (connection.isAuthenticated()) {
157            try {
158                reload();
159            }
160            catch (SmackException e) {
161                LOGGER.log(Level.SEVERE, "Could not reload Roster", e);
162            }
163        }
164        connection.addConnectionListener(new AbstractConnectionListener() {
165            public void authenticated(XMPPConnection connection) {
166                // Anonymous users can't have a roster, but it is possible that a Roster instance is
167                // retrieved if getRoster() is called *before* connect(). So we have to check here
168                // again if it's an anonymous connection.
169                if (connection.isAnonymous())
170                    return;
171                if (!connection.getConfiguration().isRosterLoadedAtLogin())
172                    return;
173                try {
174                    Roster.this.reload();
175                }
176                catch (SmackException e) {
177                    LOGGER.log(Level.SEVERE, "Could not reload Roster", e);
178                    return;
179                }
180            }
181        });
182    }
183
184    /**
185     * Returns the subscription processing mode, which dictates what action
186     * Smack will take when subscription requests from other users are made.
187     * The default subscription mode is {@link SubscriptionMode#accept_all}.<p>
188     * <p/>
189     * If using the manual mode, a PacketListener should be registered that
190     * listens for Presence packets that have a type of
191     * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}.
192     *
193     * @return the subscription mode.
194     */
195    public SubscriptionMode getSubscriptionMode() {
196        return subscriptionMode;
197    }
198
199    /**
200     * Sets the subscription processing mode, which dictates what action
201     * Smack will take when subscription requests from other users are made.
202     * The default subscription mode is {@link SubscriptionMode#accept_all}.<p>
203     * <p/>
204     * If using the manual mode, a PacketListener should be registered that
205     * listens for Presence packets that have a type of
206     * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}.
207     *
208     * @param subscriptionMode the subscription mode.
209     */
210    public void setSubscriptionMode(SubscriptionMode subscriptionMode) {
211        this.subscriptionMode = subscriptionMode;
212    }
213
214    /**
215     * Reloads the entire roster from the server. This is an asynchronous operation,
216     * which means the method will return immediately, and the roster will be
217     * reloaded at a later point when the server responds to the reload request.
218     * @throws NotLoggedInException If not logged in.
219     * @throws NotConnectedException 
220     */
221    public void reload() throws NotLoggedInException, NotConnectedException{
222        if (!connection.isAuthenticated()) {
223            throw new NotLoggedInException();
224        }
225        if (connection.isAnonymous()) {
226            throw new IllegalStateException("Anonymous users can't have a roster.");
227        }
228
229        RosterPacket packet = new RosterPacket();
230        if (rosterStore != null && connection.isRosterVersioningSupported()) {
231            packet.setVersion(rosterStore.getRosterVersion());
232        }
233        PacketFilter filter = new IQReplyFilter(packet, connection);
234        connection.addPacketListener(new RosterResultListener(), filter);
235        connection.sendPacket(packet);
236    }
237
238    /**
239     * Adds a listener to this roster. The listener will be fired anytime one or more
240     * changes to the roster are pushed from the server.
241     *
242     * @param rosterListener a roster listener.
243     */
244    public void addRosterListener(RosterListener rosterListener) {
245        if (!rosterListeners.contains(rosterListener)) {
246            rosterListeners.add(rosterListener);
247        }
248    }
249
250    /**
251     * Removes a listener from this roster. The listener will be fired anytime one or more
252     * changes to the roster are pushed from the server.
253     *
254     * @param rosterListener a roster listener.
255     */
256    public void removeRosterListener(RosterListener rosterListener) {
257        rosterListeners.remove(rosterListener);
258    }
259
260    /**
261     * Creates a new group.<p>
262     * <p/>
263     * Note: you must add at least one entry to the group for the group to be kept
264     * after a logout/login. This is due to the way that XMPP stores group information.
265     *
266     * @param name the name of the group.
267     * @return a new group, or null if the group already exists
268     * @throws IllegalStateException if logged in anonymously
269     */
270    public RosterGroup createGroup(String name) {
271        if (connection.isAnonymous()) {
272            throw new IllegalStateException("Anonymous users can't have a roster.");
273        }
274        if (groups.containsKey(name)) {
275            return groups.get(name);
276        }
277        
278        RosterGroup group = new RosterGroup(name, connection);
279        groups.put(name, group);
280        return group;
281    }
282
283    /**
284     * Creates a new roster entry and presence subscription. The server will asynchronously
285     * update the roster with the subscription status.
286     *
287     * @param user   the user. (e.g. johndoe@jabber.org)
288     * @param name   the nickname of the user.
289     * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the
290     *               the roster entry won't belong to a group.
291     * @throws NoResponseException if there was no response from the server.
292     * @throws XMPPErrorException if an XMPP exception occurs.
293     * @throws NotLoggedInException If not logged in.
294     * @throws NotConnectedException 
295     */
296    public void createEntry(String user, String name, String[] groups) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException {
297        if (!connection.isAuthenticated()) {
298            throw new NotLoggedInException();
299        }
300        if (connection.isAnonymous()) {
301            throw new IllegalStateException("Anonymous users can't have a roster.");
302        }
303
304        // Create and send roster entry creation packet.
305        RosterPacket rosterPacket = new RosterPacket();
306        rosterPacket.setType(IQ.Type.SET);
307        RosterPacket.Item item = new RosterPacket.Item(user, name);
308        if (groups != null) {
309            for (String group : groups) {
310                if (group != null && group.trim().length() > 0) {
311                    item.addGroupName(group);
312                }
313            }
314        }
315        rosterPacket.addRosterItem(item);
316        connection.createPacketCollectorAndSend(rosterPacket).nextResultOrThrow();
317
318        // Create a presence subscription packet and send.
319        Presence presencePacket = new Presence(Presence.Type.subscribe);
320        presencePacket.setTo(user);
321        connection.sendPacket(presencePacket);
322    }
323
324    /**
325     * Removes a roster entry from the roster. The roster entry will also be removed from the
326     * unfiled entries or from any roster group where it could belong and will no longer be part
327     * of the roster. Note that this is a synchronous call -- Smack must wait for the server
328     * to send an updated subscription status.
329     *
330     * @param entry a roster entry.
331     * @throws XMPPErrorException if an XMPP error occurs.
332     * @throws NotLoggedInException if not logged in.
333     * @throws NoResponseException SmackException if there was no response from the server.
334     * @throws NotConnectedException 
335     * @throws IllegalStateException if connection is not logged in or logged in anonymously
336     */
337    public void removeEntry(RosterEntry entry) throws NotLoggedInException, NoResponseException, XMPPErrorException, NotConnectedException {
338        if (!connection.isAuthenticated()) {
339            throw new NotLoggedInException();
340        }
341        if (connection.isAnonymous()) {
342            throw new IllegalStateException("Anonymous users can't have a roster.");
343        }
344
345        // Only remove the entry if it's in the entry list.
346        // The actual removal logic takes place in RosterPacketListenerprocess>>Packet(Packet)
347        if (!entries.containsKey(entry.getUser())) {
348            return;
349        }
350        RosterPacket packet = new RosterPacket();
351        packet.setType(IQ.Type.SET);
352        RosterPacket.Item item = RosterEntry.toRosterItem(entry);
353        // Set the item type as REMOVE so that the server will delete the entry
354        item.setItemType(RosterPacket.ItemType.remove);
355        packet.addRosterItem(item);
356        connection.createPacketCollectorAndSend(packet).nextResultOrThrow();
357    }
358
359    /**
360     * Returns a count of the entries in the roster.
361     *
362     * @return the number of entries in the roster.
363     */
364    public int getEntryCount() {
365        return getEntries().size();
366    }
367
368    /**
369     * Returns an unmodifiable collection of all entries in the roster, including entries
370     * that don't belong to any groups.
371     *
372     * @return all entries in the roster.
373     */
374    public Collection<RosterEntry> getEntries() {
375        Set<RosterEntry> allEntries = new HashSet<RosterEntry>();
376        // Loop through all roster groups and add their entries to the answer
377        for (RosterGroup rosterGroup : getGroups()) {
378            allEntries.addAll(rosterGroup.getEntries());
379        }
380        // Add the roster unfiled entries to the answer
381        allEntries.addAll(unfiledEntries);
382
383        return Collections.unmodifiableCollection(allEntries);
384    }
385
386    /**
387     * Returns a count of the unfiled entries in the roster. An unfiled entry is
388     * an entry that doesn't belong to any groups.
389     *
390     * @return the number of unfiled entries in the roster.
391     */
392    public int getUnfiledEntryCount() {
393        return unfiledEntries.size();
394    }
395
396    /**
397     * Returns an unmodifiable collection for the unfiled roster entries. An unfiled entry is
398     * an entry that doesn't belong to any groups.
399     *
400     * @return the unfiled roster entries.
401     */
402    public Collection<RosterEntry> getUnfiledEntries() {
403        return Collections.unmodifiableList(unfiledEntries);
404    }
405
406    /**
407     * Returns the roster entry associated with the given XMPP address or
408     * <tt>null</tt> if the user is not an entry in the roster.
409     *
410     * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be
411     *             in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource").
412     * @return the roster entry or <tt>null</tt> if it does not exist.
413     */
414    public RosterEntry getEntry(String user) {
415        if (user == null) {
416            return null;
417        }
418        return entries.get(user.toLowerCase(Locale.US));
419    }
420
421    /**
422     * Returns true if the specified XMPP address is an entry in the roster.
423     *
424     * @param user the XMPP address of the user (eg "jsmith@example.com"). The
425     *             address could be in any valid format (e.g. "domain/resource",
426     *             "user@domain" or "user@domain/resource").
427     * @return true if the XMPP address is an entry in the roster.
428     */
429    public boolean contains(String user) {
430        return getEntry(user) != null;
431    }
432
433    /**
434     * Returns the roster group with the specified name, or <tt>null</tt> if the
435     * group doesn't exist.
436     *
437     * @param name the name of the group.
438     * @return the roster group with the specified name.
439     */
440    public RosterGroup getGroup(String name) {
441        return groups.get(name);
442    }
443
444    /**
445     * Returns the number of the groups in the roster.
446     *
447     * @return the number of groups in the roster.
448     */
449    public int getGroupCount() {
450        return groups.size();
451    }
452
453    /**
454     * Returns an unmodifiable collections of all the roster groups.
455     *
456     * @return an iterator for all roster groups.
457     */
458    public Collection<RosterGroup> getGroups() {
459        return Collections.unmodifiableCollection(groups.values());
460    }
461
462    /**
463     * Returns the presence info for a particular user. If the user is offline, or
464     * if no presence data is available (such as when you are not subscribed to the
465     * user's presence updates), unavailable presence will be returned.<p>
466     * <p/>
467     * If the user has several presences (one for each resource), then the presence with
468     * highest priority will be returned. If multiple presences have the same priority,
469     * the one with the "most available" presence mode will be returned. In order,
470     * that's {@link org.jivesoftware.smack.packet.Presence.Mode#chat free to chat},
471     * {@link org.jivesoftware.smack.packet.Presence.Mode#available available},
472     * {@link org.jivesoftware.smack.packet.Presence.Mode#away away},
473     * {@link org.jivesoftware.smack.packet.Presence.Mode#xa extended away}, and
474     * {@link org.jivesoftware.smack.packet.Presence.Mode#dnd do not disturb}.<p>
475     * <p/>
476     * Note that presence information is received asynchronously. So, just after logging
477     * in to the server, presence values for users in the roster may be unavailable
478     * even if they are actually online. In other words, the value returned by this
479     * method should only be treated as a snapshot in time, and may not accurately reflect
480     * other user's presence instant by instant. If you need to track presence over time,
481     * such as when showing a visual representation of the roster, consider using a
482     * {@link RosterListener}.
483     *
484     * @param user an XMPP ID. The address could be in any valid format (e.g.
485     *             "domain/resource", "user@domain" or "user@domain/resource"). Any resource
486     *             information that's part of the ID will be discarded.
487     * @return the user's current presence, or unavailable presence if the user is offline
488     *         or if no presence information is available..
489     */
490    public Presence getPresence(String user) {
491        String key = getPresenceMapKey(StringUtils.parseBareAddress(user));
492        Map<String, Presence> userPresences = presenceMap.get(key);
493        if (userPresences == null) {
494            Presence presence = new Presence(Presence.Type.unavailable);
495            presence.setFrom(user);
496            return presence;
497        }
498        else {
499            // Find the resource with the highest priority
500            // Might be changed to use the resource with the highest availability instead.
501            Presence presence = null;
502
503            for (String resource : userPresences.keySet()) {
504                Presence p = userPresences.get(resource);
505                if (!p.isAvailable()) {
506                    continue;
507                }
508                // Chose presence with highest priority first.
509                if (presence == null || p.getPriority() > presence.getPriority()) {
510                    presence = p;
511                }
512                // If equal priority, choose "most available" by the mode value.
513                else if (p.getPriority() == presence.getPriority()) {
514                    Presence.Mode pMode = p.getMode();
515                    // Default to presence mode of available.
516                    if (pMode == null) {
517                        pMode = Presence.Mode.available;
518                    }
519                    Presence.Mode presenceMode = presence.getMode();
520                    // Default to presence mode of available.
521                    if (presenceMode == null) {
522                        presenceMode = Presence.Mode.available;
523                    }
524                    if (pMode.compareTo(presenceMode) < 0) {
525                        presence = p;
526                    }
527                }
528            }
529            if (presence == null) {
530                presence = new Presence(Presence.Type.unavailable);
531                presence.setFrom(user);
532                return presence;
533            }
534            else {
535                return presence;
536            }
537        }
538    }
539
540    /**
541     * Returns the presence info for a particular user's resource, or unavailable presence
542     * if the user is offline or if no presence information is available, such as
543     * when you are not subscribed to the user's presence updates.
544     *
545     * @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource).
546     * @return the user's current presence, or unavailable presence if the user is offline
547     *         or if no presence information is available.
548     */
549    public Presence getPresenceResource(String userWithResource) {
550        String key = getPresenceMapKey(userWithResource);
551        String resource = StringUtils.parseResource(userWithResource);
552        Map<String, Presence> userPresences = presenceMap.get(key);
553        if (userPresences == null) {
554            Presence presence = new Presence(Presence.Type.unavailable);
555            presence.setFrom(userWithResource);
556            return presence;
557        }
558        else {
559            Presence presence = userPresences.get(resource);
560            if (presence == null) {
561                presence = new Presence(Presence.Type.unavailable);
562                presence.setFrom(userWithResource);
563                return presence;
564            }
565            else {
566                return presence;
567            }
568        }
569    }
570
571    /**
572     * Returns a List of Presence objects for all of a user's current presences
573     * or an unavailable presence if the user is unavailable (offline) or if no presence
574     * information is available, such as when you are not subscribed to the user's presence
575     * updates.
576     *
577     * @param user a XMPP ID, e.g. jdoe@example.com.
578     * @return a List of Presence objects for all the user's current presences,
579     *         or an unavailable presence if the user is offline or if no presence information
580     *         is available.
581     */
582    public List<Presence> getPresences(String user) {
583        List<Presence> res;
584        String key = getPresenceMapKey(user);
585        Map<String, Presence> userPresences = presenceMap.get(key);
586        if (userPresences == null) {
587            Presence presence = new Presence(Presence.Type.unavailable);
588            presence.setFrom(user);
589            res = Arrays.asList(presence);
590        }
591        else {
592            List<Presence> answer = new ArrayList<Presence>();
593            for (Presence presence : userPresences.values()) {
594                if (presence.isAvailable()) {
595                    answer.add(presence);
596                }
597            }
598            if (!answer.isEmpty()) {
599                res = answer;
600            }
601            else {
602                Presence presence = new Presence(Presence.Type.unavailable);
603                presence.setFrom(user);
604                res = Arrays.asList(presence);
605            }
606        }
607        return Collections.unmodifiableList(res);
608    }
609
610    /**
611     * Returns the key to use in the presenceMap for a fully qualified XMPP ID.
612     * The roster can contain any valid address format such us "domain/resource",
613     * "user@domain" or "user@domain/resource". If the roster contains an entry
614     * associated with the fully qualified XMPP ID then use the fully qualified XMPP
615     * ID as the key in presenceMap, otherwise use the bare address. Note: When the
616     * key in presenceMap is a fully qualified XMPP ID, the userPresences is useless
617     * since it will always contain one entry for the user.
618     *
619     * @param user the bare or fully qualified XMPP ID, e.g. jdoe@example.com or
620     *             jdoe@example.com/Work.
621     * @return the key to use in the presenceMap for the fully qualified XMPP ID.
622     */
623    private String getPresenceMapKey(String user) {
624        if (user == null) {
625            return null;
626        }
627        String key = user;
628        if (!contains(user)) {
629            key = StringUtils.parseBareAddress(user);
630        }
631        return key.toLowerCase(Locale.US);
632    }
633
634    /**
635     * Changes the presence of available contacts offline by simulating an unavailable
636     * presence sent from the server. After a disconnection, every Presence is set
637     * to offline.
638     * @throws NotConnectedException 
639     */
640    private void setOfflinePresences() throws NotConnectedException {
641        Presence packetUnavailable;
642        for (String user : presenceMap.keySet()) {
643            Map<String, Presence> resources = presenceMap.get(user);
644            if (resources != null) {
645                for (String resource : resources.keySet()) {
646                    packetUnavailable = new Presence(Presence.Type.unavailable);
647                    packetUnavailable.setFrom(user + "/" + resource);
648                    presencePacketListener.processPacket(packetUnavailable);
649                }
650            }
651        }
652    }
653
654    /**
655     * Fires roster changed event to roster listeners indicating that the
656     * specified collections of contacts have been added, updated or deleted
657     * from the roster.
658     *
659     * @param addedEntries   the collection of address of the added contacts.
660     * @param updatedEntries the collection of address of the updated contacts.
661     * @param deletedEntries the collection of address of the deleted contacts.
662     */
663    private void fireRosterChangedEvent(Collection<String> addedEntries, Collection<String> updatedEntries,
664            Collection<String> deletedEntries) {
665        for (RosterListener listener : rosterListeners) {
666            if (!addedEntries.isEmpty()) {
667                listener.entriesAdded(addedEntries);
668            }
669            if (!updatedEntries.isEmpty()) {
670                listener.entriesUpdated(updatedEntries);
671            }
672            if (!deletedEntries.isEmpty()) {
673                listener.entriesDeleted(deletedEntries);
674            }
675        }
676    }
677
678    /**
679     * Fires roster presence changed event to roster listeners.
680     *
681     * @param presence the presence change.
682     */
683    private void fireRosterPresenceEvent(Presence presence) {
684        for (RosterListener listener : rosterListeners) {
685            listener.presenceChanged(presence);
686        }
687    }
688
689    private void addUpdateEntry(Collection<String> addedEntries, Collection<String> updatedEntries,
690                    Collection<String> unchangedEntries, RosterPacket.Item item, RosterEntry entry) {
691        RosterEntry oldEntry = entries.put(item.getUser(), entry);
692        if (oldEntry == null) {
693            addedEntries.add(item.getUser());
694        }
695        else {
696            RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry);
697            if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) {
698                updatedEntries.add(item.getUser());
699            } else {
700                // Record the entry as unchanged, so that it doesn't end up as deleted entry
701                unchangedEntries.add(item.getUser());
702            }
703        }
704
705        // Mark the entry as unfiled if it does not belong to any groups.
706        if (item.getGroupNames().isEmpty()) {
707            unfiledEntries.remove(entry);
708            unfiledEntries.add(entry);
709        }
710        else {
711            unfiledEntries.remove(entry);
712        }
713
714        // Add the user to the new groups
715
716        // Add the entry to the groups
717        List<String> newGroupNames = new ArrayList<String>();
718        for (String groupName : item.getGroupNames()) {
719            // Add the group name to the list.
720            newGroupNames.add(groupName);
721
722            // Add the entry to the group.
723            RosterGroup group = getGroup(groupName);
724            if (group == null) {
725                group = createGroup(groupName);
726                groups.put(groupName, group);
727            }
728            // Add the entry.
729            group.addEntryLocal(entry);
730        }
731
732        // Remove user from the remaining groups.
733        List<String> oldGroupNames = new ArrayList<String>();
734        for (RosterGroup group: getGroups()) {
735            oldGroupNames.add(group.getName());
736        }
737        oldGroupNames.removeAll(newGroupNames);
738
739        for (String groupName : oldGroupNames) {
740            RosterGroup group = getGroup(groupName);
741            group.removeEntryLocal(entry);
742            if (group.getEntryCount() == 0) {
743                groups.remove(groupName);
744            }
745        }
746    }
747
748    private void deleteEntry(Collection<String> deletedEntries, RosterEntry entry) {
749        String user = entry.getUser();
750        entries.remove(user);
751        unfiledEntries.remove(entry);
752        presenceMap.remove(StringUtils.parseBareAddress(user));
753        deletedEntries.add(user);
754
755        for (Entry<String,RosterGroup> e: groups.entrySet()) {
756            RosterGroup group = e.getValue();
757            group.removeEntryLocal(entry);
758            if (group.getEntryCount() == 0) {
759                groups.remove(e.getKey());
760            }
761        }
762    }
763
764
765    /**
766     * Removes all the groups with no entries.
767     *
768     * This is used by {@link RosterPushListener} and {@link RosterResultListener} to
769     * cleanup groups after removing contacts.
770     */
771    private void removeEmptyGroups() {
772        // We have to do this because RosterGroup.removeEntry removes the entry immediately
773        // (locally) and the group could remain empty.
774        // TODO Check the performance/logic for rosters with large number of groups
775        for (RosterGroup group : getGroups()) {
776            if (group.getEntryCount() == 0) {
777                groups.remove(group.getName());
778            }
779        }
780    }
781
782    /**
783     * Ignore ItemTypes as of RFC 6121, 2.1.2.5.
784     *
785     * This is used by {@link RosterPushListener} and {@link RosterResultListener}.
786     * */
787    private static boolean hasValidSubscriptionType(RosterPacket.Item item) {
788        return item.getItemType().equals(RosterPacket.ItemType.none)
789                || item.getItemType().equals(RosterPacket.ItemType.from)
790                || item.getItemType().equals(RosterPacket.ItemType.to)
791                || item.getItemType().equals(RosterPacket.ItemType.both);
792    }
793
794    /**
795     * An enumeration for the subscription mode options.
796     */
797    public enum SubscriptionMode {
798
799        /**
800         * Automatically accept all subscription and unsubscription requests. This is
801         * the default mode and is suitable for simple client. More complex client will
802         * likely wish to handle subscription requests manually.
803         */
804        accept_all,
805
806        /**
807         * Automatically reject all subscription requests.
808         */
809        reject_all,
810
811        /**
812         * Subscription requests are ignored, which means they must be manually
813         * processed by registering a listener for presence packets and then looking
814         * for any presence requests that have the type Presence.Type.SUBSCRIBE or
815         * Presence.Type.UNSUBSCRIBE.
816         */
817        manual
818    }
819
820    /**
821     * Listens for all presence packets and processes them.
822     */
823    private class PresencePacketListener implements PacketListener {
824
825        public void processPacket(Packet packet) throws NotConnectedException {
826            Presence presence = (Presence) packet;
827            String from = presence.getFrom();
828            String key = getPresenceMapKey(from);
829
830            // If an "available" presence, add it to the presence map. Each presence
831            // map will hold for a particular user a map with the presence
832            // packets saved for each resource.
833            if (presence.getType() == Presence.Type.available) {
834                Map<String, Presence> userPresences;
835                // Get the user presence map
836                if (presenceMap.get(key) == null) {
837                    userPresences = new ConcurrentHashMap<String, Presence>();
838                    presenceMap.put(key, userPresences);
839                }
840                else {
841                    userPresences = presenceMap.get(key);
842                }
843                // See if an offline presence was being stored in the map. If so, remove
844                // it since we now have an online presence.
845                userPresences.remove("");
846                // Add the new presence, using the resources as a key.
847                userPresences.put(StringUtils.parseResource(from), presence);
848                // If the user is in the roster, fire an event.
849                RosterEntry entry = entries.get(key);
850                if (entry != null) {
851                    fireRosterPresenceEvent(presence);
852                }
853            }
854            // If an "unavailable" packet.
855            else if (presence.getType() == Presence.Type.unavailable) {
856                // If no resource, this is likely an offline presence as part of
857                // a roster presence flood. In that case, we store it.
858                if ("".equals(StringUtils.parseResource(from))) {
859                    Map<String, Presence> userPresences;
860                    // Get the user presence map
861                    if (presenceMap.get(key) == null) {
862                        userPresences = new ConcurrentHashMap<String, Presence>();
863                        presenceMap.put(key, userPresences);
864                    }
865                    else {
866                        userPresences = presenceMap.get(key);
867                    }
868                    userPresences.put("", presence);
869                }
870                // Otherwise, this is a normal offline presence.
871                else if (presenceMap.get(key) != null) {
872                    Map<String, Presence> userPresences = presenceMap.get(key);
873                    // Store the offline presence, as it may include extra information
874                    // such as the user being on vacation.
875                    userPresences.put(StringUtils.parseResource(from), presence);
876                }
877                // If the user is in the roster, fire an event.
878                RosterEntry entry = entries.get(key);
879                if (entry != null) {
880                    fireRosterPresenceEvent(presence);
881                }
882            }
883            else if (presence.getType() == Presence.Type.subscribe) {
884                Presence response = null;
885                switch (subscriptionMode) {
886                case accept_all:
887                    // Accept all subscription requests.
888                    response = new Presence(Presence.Type.subscribed);
889                    break;
890                case reject_all:
891                    // Reject all subscription requests.
892                    response = new Presence(Presence.Type.unsubscribed);
893                    break;
894                case manual:
895                default:
896                    // Otherwise, in manual mode so ignore.
897                    break;
898                }
899                if (response != null) {
900                    response.setTo(presence.getFrom());
901                    connection.sendPacket(response);
902                }
903            }
904            else if (presence.getType() == Presence.Type.unsubscribe) {
905                if (subscriptionMode != SubscriptionMode.manual) {
906                    // Acknowledge and accept unsubscription notification so that the
907                    // server will stop sending notifications saying that the contact
908                    // has unsubscribed to our presence.
909                    Presence response = new Presence(Presence.Type.unsubscribed);
910                    response.setTo(presence.getFrom());
911                    connection.sendPacket(response);
912                }
913                // Otherwise, in manual mode so ignore.
914            }
915            // Error presence packets from a bare JID mean we invalidate all existing
916            // presence info for the user.
917            else if (presence.getType() == Presence.Type.error &&
918                    "".equals(StringUtils.parseResource(from)))
919            {
920                Map<String, Presence> userPresences;
921                if (!presenceMap.containsKey(key)) {
922                    userPresences = new ConcurrentHashMap<String, Presence>();
923                    presenceMap.put(key, userPresences);
924                }
925                else {
926                    userPresences = presenceMap.get(key);
927                    // Any other presence data is invalidated by the error packet.
928                    userPresences.clear();
929                }
930                // Set the new presence using the empty resource as a key.
931                userPresences.put("", presence);
932                // If the user is in the roster, fire an event.
933                RosterEntry entry = entries.get(key);
934                if (entry != null) {
935                    fireRosterPresenceEvent(presence);
936                }
937            }
938        }
939    }
940
941    /**
942     * Handles the case of the empty IQ-result for roster versioning.
943     *
944     * Intended to listen for a concrete roster result and deregisters
945     * itself after a processed packet.
946     */
947    private class RosterResultListener implements PacketListener {
948
949        @Override
950        public void processPacket(Packet packet) {
951            connection.removePacketListener(this);
952
953            IQ result = (IQ)packet;
954            if (!result.getType().equals(IQ.Type.RESULT)) {
955                LOGGER.severe("Roster result IQ not of type result. Packet: " + result.toXML());
956                return;
957            }
958
959            Collection<String> addedEntries = new ArrayList<String>();
960            Collection<String> updatedEntries = new ArrayList<String>();
961            Collection<String> deletedEntries = new ArrayList<String>();
962            Collection<String> unchangedEntries = new ArrayList<String>();
963
964            if (packet instanceof RosterPacket) {
965                // Non-empty roster result. This stanza contains all the roster elements.
966                RosterPacket rosterPacket = (RosterPacket) packet;
967
968                String version = rosterPacket.getVersion();
969
970                // Ignore items without valid subscription type
971                ArrayList<Item> validItems = new ArrayList<RosterPacket.Item>();
972                for (RosterPacket.Item item : rosterPacket.getRosterItems()) {
973                    if (hasValidSubscriptionType(item)) {
974                        validItems.add(item);
975                    }
976                }
977
978                for (RosterPacket.Item item : validItems) {
979                    RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
980                            item.getItemType(), item.getItemStatus(), Roster.this, connection);
981                    addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
982                }
983
984                // Delete all entries which where not added or updated
985                Set<String> toDelete = new HashSet<String>();
986                for (RosterEntry entry : entries.values()) {
987                    toDelete.add(entry.getUser());
988                }
989                toDelete.removeAll(addedEntries);
990                toDelete.removeAll(updatedEntries);
991                toDelete.removeAll(unchangedEntries);
992                for (String user : toDelete) {
993                    deleteEntry(deletedEntries, entries.get(user));
994                }
995
996                if (rosterStore != null) {
997                    rosterStore.resetEntries(validItems, version);
998                }
999
1000                removeEmptyGroups();
1001            }
1002            else {
1003                // Empty roster result as defined in RFC6121 2.6.3. An empty roster result basically
1004                // means that rosterver was used and the roster hasn't changed (much) since the
1005                // version we presented the server. So we simply load the roster from the store and
1006                // await possible further roster pushes.
1007                for (RosterPacket.Item item : rosterStore.getEntries()) {
1008                    RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
1009                            item.getItemType(), item.getItemStatus(), Roster.this, connection);
1010                    addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
1011                }
1012            }
1013
1014            rosterInitialized = true;
1015            synchronized (Roster.this) {
1016                Roster.this.notifyAll();
1017            }
1018            // Fire event for roster listeners.
1019            fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
1020        }
1021    }
1022
1023    /**
1024     * Listens for all roster pushes and processes them.
1025     */
1026    private class RosterPushListener implements PacketListener {
1027
1028        public void processPacket(Packet packet) throws NotConnectedException {
1029            RosterPacket rosterPacket = (RosterPacket) packet;
1030
1031            String version = rosterPacket.getVersion();
1032
1033            // Roster push (RFC 6121, 2.1.6)
1034            // A roster push with a non-empty from not matching our address MUST be ignored
1035            String jid = StringUtils.parseBareAddress(connection.getUser());
1036            if (rosterPacket.getFrom() != null &&
1037                    !rosterPacket.getFrom().equals(jid)) {
1038                LOGGER.warning("Ignoring roster push with a non matching 'from' ourJid=" + jid
1039                                + " from=" + rosterPacket.getFrom());
1040                return;
1041            }
1042
1043            // A roster push must contain exactly one entry
1044            Collection<Item> items = rosterPacket.getRosterItems();
1045            if (items.size() != 1) {
1046                LOGGER.warning("Ignoring roster push with not exaclty one entry. size=" + items.size());
1047                return;
1048            }
1049
1050            Collection<String> addedEntries = new ArrayList<String>();
1051            Collection<String> updatedEntries = new ArrayList<String>();
1052            Collection<String> deletedEntries = new ArrayList<String>();
1053            Collection<String> unchangedEntries = new ArrayList<String>();
1054
1055            // We assured abouve that the size of items is exaclty 1, therefore we are able to
1056            // safely retrieve this single item here.
1057            Item item = items.iterator().next();
1058            RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
1059                            item.getItemType(), item.getItemStatus(), Roster.this, connection);
1060
1061            if (item.getItemType().equals(RosterPacket.ItemType.remove)) {
1062                deleteEntry(deletedEntries, entry);
1063                if (rosterStore != null) {
1064                    rosterStore.removeEntry(entry.getUser(), version);
1065                }
1066            }
1067            else if (hasValidSubscriptionType(item)) {
1068                addUpdateEntry(addedEntries, updatedEntries, unchangedEntries, item, entry);
1069                if (rosterStore != null) {
1070                    rosterStore.addEntry(item, version);
1071                }
1072            }
1073            connection.sendPacket(IQ.createResultIQ(rosterPacket));
1074
1075            removeEmptyGroups();
1076
1077            // Fire event for roster listeners.
1078            fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
1079        }
1080    }
1081}