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