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.roster.packet.RosterPacket;
033import org.jivesoftware.smack.util.EqualsUtil;
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 if the XMPP connection is not connected.
097     * @throws XMPPErrorException if there was an XMPP error returned.
098     * @throws NoResponseException if there was no response from the remote entity.
099     * @throws InterruptedException if the calling thread was interrupted.
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().sendIqRequestAndWaitForResponse(packet);
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 if the XMPP connection is not connected.
216     * @throws InterruptedException if the calling thread was interrupted.
217     * @since 4.2
218     */
219    public void cancelSubscription() throws NotConnectedException, InterruptedException {
220        XMPPConnection connection = connection();
221        Presence unsubscribed = connection.getStanzaFactory().buildPresenceStanza()
222                .to(item.getJid())
223                .ofType(Presence.Type.unsubscribed)
224                .build();
225        connection.sendStanza(unsubscribed);
226    }
227
228    @Override
229    public String toString() {
230        StringBuilder buf = new StringBuilder();
231        if (getName() != null) {
232            buf.append(getName()).append(": ");
233        }
234        buf.append(getJid());
235        Collection<RosterGroup> groups = getGroups();
236        if (!groups.isEmpty()) {
237            buf.append(" [");
238            Iterator<RosterGroup> iter = groups.iterator();
239            RosterGroup group = iter.next();
240            buf.append(group.getName());
241            while (iter.hasNext()) {
242            buf.append(", ");
243                group = iter.next();
244                buf.append(group.getName());
245            }
246            buf.append(']');
247        }
248        return buf.toString();
249    }
250
251    @Override
252    public int hashCode() {
253        return getJid().hashCode();
254    }
255
256    @Override
257    public boolean equals(Object object) {
258        return EqualsUtil.equals(this, object, (e, o) ->
259            e.append(getJid(), o.getJid())
260        );
261    }
262
263    /**
264     * Indicates whether some other object is "equal to" this by comparing all members.
265     * <p>
266     * The {@link #equals(Object)} method returns <code>true</code> if the user JIDs are equal.
267     *
268     * @param obj the reference object with which to compare.
269     * @return <code>true</code> if this object is the same as the obj argument; <code>false</code>
270     *         otherwise.
271     */
272    public boolean equalsDeep(Object obj) {
273        return EqualsUtil.equals(this, obj, (e, o) ->
274            e.append(item, o.item)
275        );
276    }
277
278    /**
279     * Convert the RosterEntry to a Roster stanza &lt;item/&gt; element.
280     *
281     * @param entry the roster entry.
282     * @return the roster item.
283     */
284    static RosterPacket.Item toRosterItem(RosterEntry entry) {
285        return toRosterItem(entry, entry.getName(), false);
286    }
287
288    /**
289     * Convert the RosterEntry to a Roster stanza &lt;item/&gt; element.
290     *
291     * @param entry the roster entry
292     * @param name the name of the roster item.
293     * @return the roster item.
294     */
295    static RosterPacket.Item toRosterItem(RosterEntry entry, String name) {
296        return toRosterItem(entry, name, false);
297    }
298
299    static RosterPacket.Item toRosterItem(RosterEntry entry, boolean includeAskAttribute) {
300        return toRosterItem(entry, entry.getName(), includeAskAttribute);
301    }
302
303    /**
304     * Convert a roster entry with the given name to a roster item. As per RFC 6121 ยง 2.1.2.2., clients MUST NOT include
305     * the 'ask' attribute, thus set {@code includeAskAttribute} to {@code false}.
306     *
307     * @param entry the roster entry.
308     * @param name the name of the roster item.
309     * @param includeAskAttribute whether or not to include the 'ask' attribute.
310     * @return the roster item.
311     */
312    private static RosterPacket.Item toRosterItem(RosterEntry entry, String name, boolean includeAskAttribute) {
313        RosterPacket.Item item = new RosterPacket.Item(entry.getJid(), name);
314        item.setItemType(entry.getType());
315        if (includeAskAttribute) {
316            item.setSubscriptionPending(entry.isSubscriptionPending());
317        }
318        item.setApproved(entry.isApproved());
319        // Set the correct group names for the item.
320        for (RosterGroup group : entry.getGroups()) {
321            item.addGroupName(group.getName());
322        }
323        return item;
324    }
325
326}