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.Set;
024
025import org.jivesoftware.smack.Manager;
026import org.jivesoftware.smack.XMPPConnection;
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.roster.packet.RosterPacket;
032import org.jxmpp.jid.Jid;
033
034/**
035 * A group of roster entries.
036 *
037 * @see Roster#getGroup(String)
038 * @author Matt Tucker
039 */
040public class RosterGroup extends Manager {
041
042    private final String name;
043    private final Set<RosterEntry> entries;
044
045    /**
046     * Creates a new roster group instance.
047     *
048     * @param name the name of the group.
049     * @param connection the connection the group belongs to.
050     */
051    RosterGroup(String name, XMPPConnection connection) {
052        super(connection);
053        this.name = name;
054        entries = new LinkedHashSet<RosterEntry>();
055    }
056
057    /**
058     * Returns the name of the group.
059     *
060     * @return the name of the group.
061     */
062    public String getName() {
063        return name;
064    }
065
066    /**
067     * Sets the name of the group. Changing the group's name is like moving all the group entries
068     * of the group to a new group specified by the new name. Since this group won't have entries 
069     * it will be removed from the roster. This means that all the references to this object will 
070     * be invalid and will need to be updated to the new group specified by the new name.
071     *
072     * @param name the name of the group.
073     * @throws NotConnectedException 
074     * @throws XMPPErrorException 
075     * @throws NoResponseException 
076     * @throws InterruptedException 
077     */
078    public void setName(String name) throws NotConnectedException, NoResponseException, XMPPErrorException, InterruptedException {
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().createStanzaCollectorAndSend(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(Jid 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 = user.asBareJid();
128        synchronized (entries) {
129            for (RosterEntry entry : entries) {
130                if (entry.getJid().equals(user)) {
131                    return entry;
132                }
133            }
134        }
135        return null;
136    }
137
138    /**
139     * Returns true if the specified entry is part of this group.
140     *
141     * @param entry a roster entry.
142     * @return true if the entry is part of this group.
143     */
144    public boolean contains(RosterEntry entry) {
145        synchronized (entries) {
146            return entries.contains(entry);
147        }
148    }
149
150    /**
151     * Returns true if the specified XMPP address is an entry in this group.
152     *
153     * @param user the XMPP address of the user.
154     * @return true if the XMPP address is an entry in this group.
155     */
156    public boolean contains(Jid user) {
157        return getEntry(user) != null;
158    }
159
160    /**
161     * Adds a roster entry to this group. If the entry was unfiled then it will be removed from 
162     * the unfiled list and will be added to this group.
163     * Note that this is a synchronous call -- Smack must wait for the server
164     * to receive the updated roster.
165     *
166     * @param entry a roster entry.
167     * @throws XMPPErrorException if an error occured while trying to add the entry to the group.
168     * @throws NoResponseException if there was no response from the server.
169     * @throws NotConnectedException 
170     * @throws InterruptedException 
171     */
172    public void addEntry(RosterEntry entry) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
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().createStanzaCollectorAndSend(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     * @throws InterruptedException 
199     */
200    public void removeEntry(RosterEntry entry) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
201        // Only remove the entry if it's in the entry list.
202        // Remove the entry locally, if we wait for RosterPacketListenerprocess>>Packet(Packet)
203        // to take place the entry will exist in the group until a packet is received from the 
204        // server.
205        synchronized (entries) {
206            if (entries.contains(entry)) {
207                RosterPacket packet = new RosterPacket();
208                packet.setType(IQ.Type.set);
209                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
210                item.removeGroupName(this.getName());
211                packet.addRosterItem(item);
212                // Wait up to a certain number of seconds for a reply from the server.
213                connection().createStanzaCollectorAndSend(packet).nextResultOrThrow();
214            }
215        }
216    }
217
218    void addEntryLocal(RosterEntry entry) {
219        // Update the entry if it is already in the list
220        synchronized (entries) {
221            entries.remove(entry);
222            entries.add(entry);
223        }
224    }
225
226    void removeEntryLocal(RosterEntry entry) {
227         // Only remove the entry if it's in the entry list.
228        synchronized (entries) {
229            if (entries.contains(entry)) {
230                entries.remove(entry);
231            }
232        }
233    }
234}