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