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