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.Collection;
022import java.util.Iterator;
023import java.util.List;
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.packet.Presence;
032import org.jivesoftware.smack.packet.Presence.Type;
033import org.jivesoftware.smack.roster.packet.RosterPacket;
034
035import org.jxmpp.jid.BareJid;
036
037
038/**
039 * Each user in your roster is represented by a roster entry, which contains the user's
040 * JID and a name or nickname you assign.
041 *
042 * @author Matt Tucker
043 * @author Florian Schmaus
044 */
045public final class RosterEntry extends Manager {
046
047    private RosterPacket.Item item;
048    final private Roster roster;
049
050    /**
051     * Creates a new roster entry.
052     *
053     * @param item the Roster Stanza's Item entry.
054     * @param roster The Roster managing this entry.
055     * @param connection a connection to the XMPP server.
056     */
057    RosterEntry(RosterPacket.Item item, Roster roster, XMPPConnection connection) {
058        super(connection);
059        this.item = item;
060        this.roster = roster;
061    }
062
063    /**
064     * Returns the JID of the user associated with this entry.
065     *
066     * @return the user associated with this entry.
067     * @deprecated use {@link #getJid()} instead.
068     */
069    @Deprecated
070    public String getUser() {
071        return getJid().toString();
072    }
073
074    /**
075     * Returns the JID associated with this entry.
076     *
077     * @return the user associated with this entry.
078     */
079    public BareJid getJid() {
080        return item.getJid();
081    }
082
083    /**
084     * Returns the name associated with this entry.
085     *
086     * @return the name.
087     */
088    public String getName() {
089        return item.getName();
090    }
091
092    /**
093     * Sets the name associated with this entry.
094     *
095     * @param name the name.
096     * @throws NotConnectedException 
097     * @throws XMPPErrorException 
098     * @throws NoResponseException 
099     * @throws InterruptedException 
100     */
101    public synchronized void setName(String name) throws NotConnectedException, NoResponseException, XMPPErrorException, InterruptedException {
102        // Do nothing if the name hasn't changed.
103        if (name != null && name.equals(getName())) {
104            return;
105        }
106
107        RosterPacket packet = new RosterPacket();
108        packet.setType(IQ.Type.set);
109
110        // Create a new roster item with the current RosterEntry and the *new* name. Note that we can't set the name of
111        // RosterEntry right away, as otherwise the updated event wont get fired, because equalsDeep would return true.
112        packet.addRosterItem(toRosterItem(this, name));
113        connection().createStanzaCollectorAndSend(packet).nextResultOrThrow();
114
115        // We have received a result response to the IQ set, the name was successfully changed
116        item.setName(name);
117    }
118
119    /**
120     * Updates the state of the entry with the new values.
121     *
122     * @param name the nickname for the entry.
123     * @param type the subscription type.
124     * @param subscriptionPending TODO
125     */
126    void updateItem(RosterPacket.Item item) {
127        assert (item != null);
128        this.item = item;
129    }
130
131    /**
132     * Returns the pre-approval state of this entry.
133     *
134     * @return the pre-approval state.
135     */
136    public boolean isApproved() {
137        return item.isApproved();
138    }
139
140    /**
141     * Returns an copied list of the roster groups that this entry belongs to.
142     *
143     * @return an iterator for the groups this entry belongs to.
144     */
145    public List<RosterGroup> getGroups() {
146        List<RosterGroup> results = new ArrayList<RosterGroup>();
147        // Loop through all roster groups and find the ones that contain this
148        // entry. This algorithm should be fine
149        for (RosterGroup group : roster.getGroups()) {
150            if (group.contains(this)) {
151                results.add(group);
152            }
153        }
154        return results;
155    }
156
157    /**
158     * Returns the roster subscription type of the entry. When the type is
159     * RosterPacket.ItemType.none or RosterPacket.ItemType.from,
160     * refer to {@link RosterEntry getStatus()} to see if a subscription request
161     * is pending.
162     *
163     * @return the type.
164     */
165    public RosterPacket.ItemType getType() {
166        return item.getItemType();
167    }
168
169    /**
170     * Returns the roster subscription request status of the entry. If
171     * {@code true}, then the contact did not answer the subscription request
172     * yet.
173     *
174     * @return the status.
175     * @since 4.2
176     */
177    public boolean isSubscriptionPending() {
178        return item.isSubscriptionPending();
179    }
180
181    /**
182     * Check if the contact is subscribed to "my" presence. This allows the contact to see the presence information.
183     *
184     * @return true if the contact has a presence subscription.
185     * @since 4.2
186     */
187    public boolean canSeeMyPresence() {
188        switch (getType()) {
189        case from:
190        case both:
191            return true;
192        default:
193            return false;
194        }
195    }
196
197    /**
198     * Check if we are subscribed to the contact's presence. If <code>true</code> then the contact has allowed us to
199     * receive presence information.
200     *
201     * @return true if we are subscribed to the contact's presence.
202     * @since 4.2
203     */
204    public boolean canSeeHisPresence() {
205        switch (getType()) {
206        case to:
207        case both:
208            return true;
209        default:
210            return false;
211        }
212    }
213
214    /**
215     * Cancel the presence subscription the XMPP entity representing this roster entry has with us.
216     * 
217     * @throws NotConnectedException
218     * @throws InterruptedException
219     * @since 4.2
220     */
221    public void cancelSubscription() throws NotConnectedException, InterruptedException {
222        Presence unsubscribed = new Presence(item.getJid(), Type.unsubscribed);
223        connection().sendStanza(unsubscribed);
224    }
225
226    @Override
227    public String toString() {
228        StringBuilder buf = new StringBuilder();
229        if (getName() != null) {
230            buf.append(getName()).append(": ");
231        }
232        buf.append(getJid());
233        Collection<RosterGroup> groups = getGroups();
234        if (!groups.isEmpty()) {
235            buf.append(" [");
236            Iterator<RosterGroup> iter = groups.iterator();
237            RosterGroup group = iter.next();
238            buf.append(group.getName());
239            while (iter.hasNext()) {
240            buf.append(", ");
241                group = iter.next();
242                buf.append(group.getName());
243            }
244            buf.append(']');
245        }
246        return buf.toString();
247    }
248
249    @Override
250    public int hashCode() {
251        return getJid().hashCode();
252    }
253
254    @Override
255    public boolean equals(Object object) {
256        if (this == object) {
257            return true;
258        }
259        if (object != null && object instanceof RosterEntry) {
260            return getJid().equals(((RosterEntry) object).getJid());
261        }
262        else {
263            return false;
264        }
265    }
266
267    /**
268     * Indicates whether some other object is "equal to" this by comparing all members.
269     * <p>
270     * The {@link #equals(Object)} method returns <code>true</code> if the user JIDs are equal.
271     * 
272     * @param obj the reference object with which to compare.
273     * @return <code>true</code> if this object is the same as the obj argument; <code>false</code>
274     *         otherwise.
275     */
276    public boolean equalsDeep(Object obj) {
277        if (this == obj)
278            return true;
279        if (obj == null)
280            return false;
281        if (getClass() != obj.getClass())
282            return false;
283        RosterEntry other = (RosterEntry) obj;
284        return other.item.equals(this.item);
285    }
286
287    /**
288     * Convert the RosterEntry to a Roster stanza &lt;item/&gt; element.
289     *
290     * @param entry the roster entry.
291     * @return the roster item.
292     */
293    static RosterPacket.Item toRosterItem(RosterEntry entry) {
294        return toRosterItem(entry, entry.getName(), false);
295    }
296
297    /**
298     * Convert the RosterEntry to a Roster stanza &lt;item/&gt; element.
299     *
300     * @param entry the roster entry
301     * @param name the name of the roster item.
302     * @return the roster item.
303     */
304    static RosterPacket.Item toRosterItem(RosterEntry entry, String name) {
305        return toRosterItem(entry, name, false);
306    }
307
308    static RosterPacket.Item toRosterItem(RosterEntry entry, boolean includeAskAttribute) {
309        return toRosterItem(entry, entry.getName(), includeAskAttribute);
310    }
311
312    /**
313     * Convert a roster entry with the given name to a roster item. As per RFC 6121 ยง 2.1.2.2., clients MUST NOT include
314     * the 'ask' attribute, thus set {@code includeAskAttribute} to {@code false}.
315     *
316     * @param entry the roster entry.
317     * @param name the name of the roster item.
318     * @param includeAskAttribute whether or not to include the 'ask' attribute.
319     * @return the roster item.
320     */
321    private static RosterPacket.Item toRosterItem(RosterEntry entry, String name, boolean includeAskAttribute) {
322        RosterPacket.Item item = new RosterPacket.Item(entry.getJid(), name);
323        item.setItemType(entry.getType());
324        if (includeAskAttribute) {
325            item.setSubscriptionPending(entry.isSubscriptionPending());
326        }
327        item.setApproved(entry.isApproved());
328        // Set the correct group names for the item.
329        for (RosterGroup group : entry.getGroups()) {
330            item.addGroupName(group.getName());
331        }
332        return item;
333    }
334
335}