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.SmackException.NoResponseException;
027import org.jivesoftware.smack.SmackException.NotConnectedException;
028import org.jivesoftware.smack.XMPPConnection;
029import org.jivesoftware.smack.XMPPException.XMPPErrorException;
030import org.jivesoftware.smack.packet.IQ;
031import org.jivesoftware.smack.roster.packet.RosterPacket;
032
033import org.jxmpp.jid.Jid;
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<>();
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 if the XMPP connection is not connected.
075     * @throws XMPPErrorException if there was an XMPP error returned.
076     * @throws NoResponseException if there was no response from the remote entity.
077     * @throws InterruptedException if the calling thread was interrupted.
078     */
079    public void setName(String name) throws NotConnectedException, NoResponseException, XMPPErrorException, InterruptedException {
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().createStanzaCollectorAndSend(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<>(entries);
112        }
113    }
114
115    /**
116     * Returns the roster entry associated with the given XMPP address or
117     * <code>null</code> 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 <code>null</code> if it does not exist in the group.
121     */
122    public RosterEntry getEntry(Jid 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 = user.asBareJid();
129        synchronized (entries) {
130            for (RosterEntry entry : entries) {
131                if (entry.getJid().equals(user)) {
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(Jid 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 occurred while trying to add the entry to the group.
169     * @throws NoResponseException if there was no response from the server.
170     * @throws NotConnectedException if the XMPP connection is not connected.
171     * @throws InterruptedException if the calling thread was interrupted.
172     */
173    public void addEntry(RosterEntry entry) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
174        // Only add the entry if it isn't already in the list.
175        synchronized (entries) {
176            if (!entries.contains(entry)) {
177                RosterPacket packet = new RosterPacket();
178                packet.setType(IQ.Type.set);
179                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
180                item.addGroupName(getName());
181                packet.addRosterItem(item);
182                // Wait up to a certain number of seconds for a reply from the server.
183                connection().createStanzaCollectorAndSend(packet).nextResultOrThrow();
184            }
185        }
186    }
187
188    /**
189     * Removes a roster entry from this group. If the entry does not belong to any other group
190     * then it will be considered as unfiled, therefore it will be added to the list of unfiled
191     * entries.
192     * Note that this is a synchronous call -- Smack must wait for the server
193     * to receive the updated roster.
194     *
195     * @param entry a roster entry.
196     * @throws XMPPErrorException if an error occurred while trying to remove the entry from the group.
197     * @throws NoResponseException if there was no response from the server.
198     * @throws NotConnectedException if the XMPP connection is not connected.
199     * @throws InterruptedException if the calling thread was interrupted.
200     */
201    public void removeEntry(RosterEntry entry) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
202        // Only remove the entry if it's in the entry list.
203        // Remove the entry locally, if we wait for RosterPacketListenerProcess>>Packet(Packet)
204        // to take place the entry will exist in the group until a packet is received from the
205        // server.
206        synchronized (entries) {
207            if (entries.contains(entry)) {
208                RosterPacket packet = new RosterPacket();
209                packet.setType(IQ.Type.set);
210                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
211                item.removeGroupName(this.getName());
212                packet.addRosterItem(item);
213                // Wait up to a certain number of seconds for a reply from the server.
214                connection().createStanzaCollectorAndSend(packet).nextResultOrThrow();
215            }
216        }
217    }
218
219    void addEntryLocal(RosterEntry entry) {
220        // Update the entry if it is already in the list
221        synchronized (entries) {
222            entries.remove(entry);
223            entries.add(entry);
224        }
225    }
226
227    void removeEntryLocal(RosterEntry entry) {
228         // Only remove the entry if it's in the entry list.
229        synchronized (entries) {
230            if (entries.contains(entry)) {
231                entries.remove(entry);
232            }
233        }
234    }
235}