1/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2003-2007 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 *     http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smack;
22
23import org.jivesoftware.smack.filter.PacketIDFilter;
24import org.jivesoftware.smack.packet.IQ;
25import org.jivesoftware.smack.packet.RosterPacket;
26import org.jivesoftware.smack.util.StringUtils;
27
28import java.util.ArrayList;
29import java.util.Collection;
30import java.util.Collections;
31import java.util.List;
32
33/**
34 * A group of roster entries.
35 *
36 * @see Roster#getGroup(String)
37 * @author Matt Tucker
38 */
39public class RosterGroup {
40
41    private String name;
42    private Connection connection;
43    private final List<RosterEntry> entries;
44
45    /**
46     * Creates a new roster group instance.
47     *
48     * @param name the name of the group.
49     * @param connection the connection the group belongs to.
50     */
51    RosterGroup(String name, Connection connection) {
52        this.name = name;
53        this.connection = connection;
54        entries = new ArrayList<RosterEntry>();
55    }
56
57    /**
58     * Returns the name of the group.
59     *
60     * @return the name of the group.
61     */
62    public String getName() {
63        return name;
64    }
65
66    /**
67     * Sets the name of the group. Changing the group's name is like moving all the group entries
68     * of the group to a new group specified by the new name. Since this group won't have entries
69     * it will be removed from the roster. This means that all the references to this object will
70     * be invalid and will need to be updated to the new group specified by the new name.
71     *
72     * @param name the name of the group.
73     */
74    public void setName(String name) {
75        synchronized (entries) {
76            for (RosterEntry entry : entries) {
77                RosterPacket packet = new RosterPacket();
78                packet.setType(IQ.Type.SET);
79                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
80                item.removeGroupName(this.name);
81                item.addGroupName(name);
82                packet.addRosterItem(item);
83                connection.sendPacket(packet);
84            }
85        }
86    }
87
88    /**
89     * Returns the number of entries in the group.
90     *
91     * @return the number of entries in the group.
92     */
93    public int getEntryCount() {
94        synchronized (entries) {
95            return entries.size();
96        }
97    }
98
99    /**
100     * Returns an unmodifiable collection of all entries in the group.
101     *
102     * @return all entries in the group.
103     */
104    public Collection<RosterEntry> getEntries() {
105        synchronized (entries) {
106            return Collections.unmodifiableList(new ArrayList<RosterEntry>(entries));
107        }
108    }
109
110    /**
111     * Returns the roster entry associated with the given XMPP address or
112     * <tt>null</tt> if the user is not an entry in the group.
113     *
114     * @param user the XMPP address of the user (eg "jsmith@example.com").
115     * @return the roster entry or <tt>null</tt> if it does not exist in the group.
116     */
117    public RosterEntry getEntry(String user) {
118        if (user == null) {
119            return null;
120        }
121        // Roster entries never include a resource so remove the resource
122        // if it's a part of the XMPP address.
123        user = StringUtils.parseBareAddress(user);
124        String userLowerCase = user.toLowerCase();
125        synchronized (entries) {
126            for (RosterEntry entry : entries) {
127                if (entry.getUser().equals(userLowerCase)) {
128                    return entry;
129                }
130            }
131        }
132        return null;
133    }
134
135    /**
136     * Returns true if the specified entry is part of this group.
137     *
138     * @param entry a roster entry.
139     * @return true if the entry is part of this group.
140     */
141    public boolean contains(RosterEntry entry) {
142        synchronized (entries) {
143            return entries.contains(entry);
144        }
145    }
146
147    /**
148     * Returns true if the specified XMPP address is an entry in this group.
149     *
150     * @param user the XMPP address of the user.
151     * @return true if the XMPP address is an entry in this group.
152     */
153    public boolean contains(String user) {
154        return getEntry(user) != null;
155    }
156
157    /**
158     * Adds a roster entry to this group. If the entry was unfiled then it will be removed from
159     * the unfiled list and will be added to this group.
160     * Note that this is an asynchronous call -- Smack must wait for the server
161     * to receive the updated roster.
162     *
163     * @param entry a roster entry.
164     * @throws XMPPException if an error occured while trying to add the entry to the group.
165     */
166    public void addEntry(RosterEntry entry) throws XMPPException {
167        PacketCollector collector = null;
168        // Only add the entry if it isn't already in the list.
169        synchronized (entries) {
170            if (!entries.contains(entry)) {
171                RosterPacket packet = new RosterPacket();
172                packet.setType(IQ.Type.SET);
173                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
174                item.addGroupName(getName());
175                packet.addRosterItem(item);
176                // Wait up to a certain number of seconds for a reply from the server.
177                collector = connection
178                        .createPacketCollector(new PacketIDFilter(packet.getPacketID()));
179                connection.sendPacket(packet);
180            }
181        }
182        if (collector != null) {
183            IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
184            collector.cancel();
185            if (response == null) {
186                throw new XMPPException("No response from the server.");
187            }
188            // If the server replied with an error, throw an exception.
189            else if (response.getType() == IQ.Type.ERROR) {
190                throw new XMPPException(response.getError());
191            }
192        }
193    }
194
195    /**
196     * Removes a roster entry from this group. If the entry does not belong to any other group
197     * then it will be considered as unfiled, therefore it will be added to the list of unfiled
198     * entries.
199     * Note that this is an asynchronous call -- Smack must wait for the server
200     * to receive the updated roster.
201     *
202     * @param entry a roster entry.
203     * @throws XMPPException if an error occured while trying to remove the entry from the group.
204     */
205    public void removeEntry(RosterEntry entry) throws XMPPException {
206        PacketCollector collector = null;
207        // Only remove the entry if it's in the entry list.
208        // Remove the entry locally, if we wait for RosterPacketListenerprocess>>Packet(Packet)
209        // to take place the entry will exist in the group until a packet is received from the
210        // server.
211        synchronized (entries) {
212            if (entries.contains(entry)) {
213                RosterPacket packet = new RosterPacket();
214                packet.setType(IQ.Type.SET);
215                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
216                item.removeGroupName(this.getName());
217                packet.addRosterItem(item);
218                // Wait up to a certain number of seconds for a reply from the server.
219                collector = connection
220                        .createPacketCollector(new PacketIDFilter(packet.getPacketID()));
221                connection.sendPacket(packet);
222            }
223        }
224        if (collector != null) {
225            IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
226            collector.cancel();
227            if (response == null) {
228                throw new XMPPException("No response from the server.");
229            }
230            // If the server replied with an error, throw an exception.
231            else if (response.getType() == IQ.Type.ERROR) {
232                throw new XMPPException(response.getError());
233            }
234        }
235    }
236
237    public void addEntryLocal(RosterEntry entry) {
238        // Only add the entry if it isn't already in the list.
239        synchronized (entries) {
240            entries.remove(entry);
241            entries.add(entry);
242        }
243    }
244
245    void removeEntryLocal(RosterEntry entry) {
246         // Only remove the entry if it's in the entry list.
247        synchronized (entries) {
248            if (entries.contains(entry)) {
249                entries.remove(entry);
250            }
251        }
252    }
253}