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.LinkedHashSet;
022import java.util.List;
023import java.util.Locale;
024import java.util.Set;
025
026import org.jivesoftware.smack.Manager;
027import org.jivesoftware.smack.XMPPConnection;
028import org.jivesoftware.smack.SmackException.NoResponseException;
029import org.jivesoftware.smack.SmackException.NotConnectedException;
030import org.jivesoftware.smack.XMPPException.XMPPErrorException;
031import org.jivesoftware.smack.packet.IQ;
032import org.jivesoftware.smack.roster.packet.RosterPacket;
033import org.jxmpp.util.XmppStringUtils;
034
035/**
036 * A group of roster entries.
037 *
038 * @see Roster#getGroup(String)
039 * @author Matt Tucker
040 */
041public class RosterGroup extends Manager {
042
043    private final String name;
044    private final Set<RosterEntry> entries;
045
046    /**
047     * Creates a new roster group instance.
048     *
049     * @param name the name of the group.
050     * @param connection the connection the group belongs to.
051     */
052    RosterGroup(String name, XMPPConnection connection) {
053        super(connection);
054        this.name = name;
055        entries = new LinkedHashSet<RosterEntry>();
056    }
057
058    /**
059     * Returns the name of the group.
060     *
061     * @return the name of the group.
062     */
063    public String getName() {
064        return name;
065    }
066
067    /**
068     * Sets the name of the group. Changing the group's name is like moving all the group entries
069     * of the group to a new group specified by the new name. Since this group won't have entries 
070     * it will be removed from the roster. This means that all the references to this object will 
071     * be invalid and will need to be updated to the new group specified by the new name.
072     *
073     * @param name the name of the group.
074     * @throws NotConnectedException 
075     * @throws XMPPErrorException 
076     * @throws NoResponseException 
077     */
078    public void setName(String name) throws NotConnectedException, NoResponseException, XMPPErrorException {
079        synchronized (entries) {
080            for (RosterEntry entry : entries) {
081                RosterPacket packet = new RosterPacket();
082                packet.setType(IQ.Type.set);
083                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
084                item.removeGroupName(this.name);
085                item.addGroupName(name);
086                packet.addRosterItem(item);
087                connection().createPacketCollectorAndSend(packet).nextResultOrThrow();
088            }
089        }
090    }
091
092    /**
093     * Returns the number of entries in the group.
094     *
095     * @return the number of entries in the group.
096     */
097    public int getEntryCount() {
098        synchronized (entries) {
099            return entries.size();
100        }
101    }
102
103    /**
104     * Returns an copied list of all entries in the group.
105     *
106     * @return all entries in the group.
107     */
108    public List<RosterEntry> getEntries() {
109        synchronized (entries) {
110            return new ArrayList<RosterEntry>(entries);
111        }
112    }
113
114    /**
115     * Returns the roster entry associated with the given XMPP address or
116     * <tt>null</tt> if the user is not an entry in the group.
117     *
118     * @param user the XMPP address of the user (eg "jsmith@example.com").
119     * @return the roster entry or <tt>null</tt> if it does not exist in the group.
120     */
121    public RosterEntry getEntry(String user) {
122        if (user == null) {
123            return null;
124        }
125        // Roster entries never include a resource so remove the resource
126        // if it's a part of the XMPP address.
127        user = XmppStringUtils.parseBareJid(user);
128        String userLowerCase = user.toLowerCase(Locale.US);
129        synchronized (entries) {
130            for (RosterEntry entry : entries) {
131                if (entry.getUser().equals(userLowerCase)) {
132                    return entry;
133                }
134            }
135        }
136        return null;
137    }
138
139    /**
140     * Returns true if the specified entry is part of this group.
141     *
142     * @param entry a roster entry.
143     * @return true if the entry is part of this group.
144     */
145    public boolean contains(RosterEntry entry) {
146        synchronized (entries) {
147            return entries.contains(entry);
148        }
149    }
150
151    /**
152     * Returns true if the specified XMPP address is an entry in this group.
153     *
154     * @param user the XMPP address of the user.
155     * @return true if the XMPP address is an entry in this group.
156     */
157    public boolean contains(String user) {
158        return getEntry(user) != null;
159    }
160
161    /**
162     * Adds a roster entry to this group. If the entry was unfiled then it will be removed from 
163     * the unfiled list and will be added to this group.
164     * Note that this is a synchronous call -- Smack must wait for the server
165     * to receive the updated roster.
166     *
167     * @param entry a roster entry.
168     * @throws XMPPErrorException if an error occured while trying to add the entry to the group.
169     * @throws NoResponseException if there was no response from the server.
170     * @throws NotConnectedException 
171     */
172    public void addEntry(RosterEntry entry) throws NoResponseException, XMPPErrorException, NotConnectedException {
173        // Only add the entry if it isn't already in the list.
174        synchronized (entries) {
175            if (!entries.contains(entry)) {
176                RosterPacket packet = new RosterPacket();
177                packet.setType(IQ.Type.set);
178                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
179                item.addGroupName(getName());
180                packet.addRosterItem(item);
181                // Wait up to a certain number of seconds for a reply from the server.
182                connection().createPacketCollectorAndSend(packet).nextResultOrThrow();
183            }
184        }
185    }
186
187    /**
188     * Removes a roster entry from this group. If the entry does not belong to any other group 
189     * then it will be considered as unfiled, therefore it will be added to the list of unfiled 
190     * entries.
191     * Note that this is a synchronous call -- Smack must wait for the server
192     * to receive the updated roster.
193     *
194     * @param entry a roster entry.
195     * @throws XMPPErrorException if an error occurred while trying to remove the entry from the group. 
196     * @throws NoResponseException if there was no response from the server.
197     * @throws NotConnectedException 
198     */
199    public void removeEntry(RosterEntry entry) throws NoResponseException, XMPPErrorException, NotConnectedException {
200        // Only remove the entry if it's in the entry list.
201        // Remove the entry locally, if we wait for RosterPacketListenerprocess>>Packet(Packet)
202        // to take place the entry will exist in the group until a packet is received from the 
203        // server.
204        synchronized (entries) {
205            if (entries.contains(entry)) {
206                RosterPacket packet = new RosterPacket();
207                packet.setType(IQ.Type.set);
208                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
209                item.removeGroupName(this.getName());
210                packet.addRosterItem(item);
211                // Wait up to a certain number of seconds for a reply from the server.
212                connection().createPacketCollectorAndSend(packet).nextResultOrThrow();
213            }
214        }
215    }
216
217    void addEntryLocal(RosterEntry entry) {
218        // Update the entry if it is already in the list
219        synchronized (entries) {
220            entries.remove(entry);
221            entries.add(entry);
222        }
223    }
224
225    void removeEntryLocal(RosterEntry entry) {
226         // Only remove the entry if it's in the entry list.
227        synchronized (entries) {
228            if (entries.contains(entry)) {
229                entries.remove(entry);
230            }
231        }
232    }
233}