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    private final 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 this entries item.
121     *
122     * @param item new item
123     */
124    void updateItem(RosterPacket.Item item) {
125        assert (item != null);
126        this.item = item;
127    }
128
129    /**
130     * Returns the pre-approval state of this entry.
131     *
132     * @return the pre-approval state.
133     */
134    public boolean isApproved() {
135        return item.isApproved();
136    }
137
138    /**
139     * Returns an copied list of the roster groups that this entry belongs to.
140     *
141     * @return an iterator for the groups this entry belongs to.
142     */
143    public List<RosterGroup> getGroups() {
144        List<RosterGroup> results = new ArrayList<>();
145        // Loop through all roster groups and find the ones that contain this
146        // entry. This algorithm should be fine
147        for (RosterGroup group : roster.getGroups()) {
148            if (group.contains(this)) {
149                results.add(group);
150            }
151        }
152        return results;
153    }
154
155    /**
156     * Returns the roster subscription type of the entry. When the type is
157     * RosterPacket.ItemType.none or RosterPacket.ItemType.from,
158     * refer to {@link RosterEntry getStatus()} to see if a subscription request
159     * is pending.
160     *
161     * @return the type.
162     */
163    public RosterPacket.ItemType getType() {
164        return item.getItemType();
165    }
166
167    /**
168     * Returns the roster subscription request status of the entry. If
169     * {@code true}, then the contact did not answer the subscription request
170     * yet.
171     *
172     * @return the status.
173     * @since 4.2
174     */
175    public boolean isSubscriptionPending() {
176        return item.isSubscriptionPending();
177    }
178
179    /**
180     * Check if the contact is subscribed to "my" presence. This allows the contact to see the presence information.
181     *
182     * @return true if the contact has a presence subscription.
183     * @since 4.2
184     */
185    public boolean canSeeMyPresence() {
186        switch (getType()) {
187        case from:
188        case both:
189            return true;
190        default:
191            return false;
192        }
193    }
194
195    /**
196     * Check if we are subscribed to the contact's presence. If <code>true</code> then the contact has allowed us to
197     * receive presence information.
198     *
199     * @return true if we are subscribed to the contact's presence.
200     * @since 4.2
201     */
202    public boolean canSeeHisPresence() {
203        switch (getType()) {
204        case to:
205        case both:
206            return true;
207        default:
208            return false;
209        }
210    }
211
212    /**
213     * Cancel the presence subscription the XMPP entity representing this roster entry has with us.
214     *
215     * @throws NotConnectedException
216     * @throws InterruptedException
217     * @since 4.2
218     */
219    public void cancelSubscription() throws NotConnectedException, InterruptedException {
220        Presence unsubscribed = new Presence(item.getJid(), Type.unsubscribed);
221        connection().sendStanza(unsubscribed);
222    }
223
224    @Override
225    public String toString() {
226        StringBuilder buf = new StringBuilder();
227        if (getName() != null) {
228            buf.append(getName()).append(": ");
229        }
230        buf.append(getJid());
231        Collection<RosterGroup> groups = getGroups();
232        if (!groups.isEmpty()) {
233            buf.append(" [");
234            Iterator<RosterGroup> iter = groups.iterator();
235            RosterGroup group = iter.next();
236            buf.append(group.getName());
237            while (iter.hasNext()) {
238            buf.append(", ");
239                group = iter.next();
240                buf.append(group.getName());
241            }
242            buf.append(']');
243        }
244        return buf.toString();
245    }
246
247    @Override
248    public int hashCode() {
249        return getJid().hashCode();
250    }
251
252    @Override
253    public boolean equals(Object object) {
254        if (this == object) {
255            return true;
256        }
257        if (object != null && object instanceof RosterEntry) {
258            return getJid().equals(((RosterEntry) object).getJid());
259        }
260        else {
261            return false;
262        }
263    }
264
265    /**
266     * Indicates whether some other object is "equal to" this by comparing all members.
267     * <p>
268     * The {@link #equals(Object)} method returns <code>true</code> if the user JIDs are equal.
269     *
270     * @param obj the reference object with which to compare.
271     * @return <code>true</code> if this object is the same as the obj argument; <code>false</code>
272     *         otherwise.
273     */
274    public boolean equalsDeep(Object obj) {
275        if (this == obj)
276            return true;
277        if (obj == null)
278            return false;
279        if (getClass() != obj.getClass())
280            return false;
281        RosterEntry other = (RosterEntry) obj;
282        return other.item.equals(this.item);
283    }
284
285    /**
286     * Convert the RosterEntry to a Roster stanza &lt;item/&gt; element.
287     *
288     * @param entry the roster entry.
289     * @return the roster item.
290     */
291    static RosterPacket.Item toRosterItem(RosterEntry entry) {
292        return toRosterItem(entry, entry.getName(), false);
293    }
294
295    /**
296     * Convert the RosterEntry to a Roster stanza &lt;item/&gt; element.
297     *
298     * @param entry the roster entry
299     * @param name the name of the roster item.
300     * @return the roster item.
301     */
302    static RosterPacket.Item toRosterItem(RosterEntry entry, String name) {
303        return toRosterItem(entry, name, false);
304    }
305
306    static RosterPacket.Item toRosterItem(RosterEntry entry, boolean includeAskAttribute) {
307        return toRosterItem(entry, entry.getName(), includeAskAttribute);
308    }
309
310    /**
311     * Convert a roster entry with the given name to a roster item. As per RFC 6121 ยง 2.1.2.2., clients MUST NOT include
312     * the 'ask' attribute, thus set {@code includeAskAttribute} to {@code false}.
313     *
314     * @param entry the roster entry.
315     * @param name the name of the roster item.
316     * @param includeAskAttribute whether or not to include the 'ask' attribute.
317     * @return the roster item.
318     */
319    private static RosterPacket.Item toRosterItem(RosterEntry entry, String name, boolean includeAskAttribute) {
320        RosterPacket.Item item = new RosterPacket.Item(entry.getJid(), name);
321        item.setItemType(entry.getType());
322        if (includeAskAttribute) {
323            item.setSubscriptionPending(entry.isSubscriptionPending());
324        }
325        item.setApproved(entry.isApproved());
326        // Set the correct group names for the item.
327        for (RosterGroup group : entry.getGroups()) {
328            item.addGroupName(group.getName());
329        }
330        return item;
331    }
332
333}