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;
034import org.jivesoftware.smack.util.EqualsUtil;
035
036import org.jxmpp.jid.BareJid;
037
038
039/**
040 * Each user in your roster is represented by a roster entry, which contains the user's
041 * JID and a name or nickname you assign.
042 *
043 * @author Matt Tucker
044 * @author Florian Schmaus
045 */
046public final class RosterEntry extends Manager {
047
048    private RosterPacket.Item item;
049    private final Roster roster;
050
051    /**
052     * Creates a new roster entry.
053     *
054     * @param item the Roster Stanza's Item entry.
055     * @param roster The Roster managing this entry.
056     * @param connection a connection to the XMPP server.
057     */
058    RosterEntry(RosterPacket.Item item, Roster roster, XMPPConnection connection) {
059        super(connection);
060        this.item = item;
061        this.roster = roster;
062    }
063
064    /**
065     * Returns the JID of the user associated with this entry.
066     *
067     * @return the user associated with this entry.
068     * @deprecated use {@link #getJid()} instead.
069     */
070    @Deprecated
071    public String getUser() {
072        return getJid().toString();
073    }
074
075    /**
076     * Returns the JID associated with this entry.
077     *
078     * @return the user associated with this entry.
079     */
080    public BareJid getJid() {
081        return item.getJid();
082    }
083
084    /**
085     * Returns the name associated with this entry.
086     *
087     * @return the name.
088     */
089    public String getName() {
090        return item.getName();
091    }
092
093    /**
094     * Sets the name associated with this entry.
095     *
096     * @param name the name.
097     * @throws NotConnectedException if the XMPP connection is not connected.
098     * @throws XMPPErrorException if there was an XMPP error returned.
099     * @throws NoResponseException if there was no response from the remote entity.
100     * @throws InterruptedException if the calling thread was interrupted.
101     */
102    public synchronized void setName(String name) throws NotConnectedException, NoResponseException, XMPPErrorException, InterruptedException {
103        // Do nothing if the name hasn't changed.
104        if (name != null && name.equals(getName())) {
105            return;
106        }
107
108        RosterPacket packet = new RosterPacket();
109        packet.setType(IQ.Type.set);
110
111        // Create a new roster item with the current RosterEntry and the *new* name. Note that we can't set the name of
112        // RosterEntry right away, as otherwise the updated event wont get fired, because equalsDeep would return true.
113        packet.addRosterItem(toRosterItem(this, name));
114        connection().createStanzaCollectorAndSend(packet).nextResultOrThrow();
115
116        // We have received a result response to the IQ set, the name was successfully changed
117        item.setName(name);
118    }
119
120    /**
121     * Updates this entries item.
122     *
123     * @param item new item
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<>();
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 if the XMPP connection is not connected.
217     * @throws InterruptedException if the calling thread was interrupted.
218     * @since 4.2
219     */
220    public void cancelSubscription() throws NotConnectedException, InterruptedException {
221        XMPPConnection connection = connection();
222        Presence unsubscribed = connection.getStanzaFactory().buildPresenceStanza()
223                .to(item.getJid())
224                .ofType(Type.unsubscribed)
225                .build();
226        connection.sendStanza(unsubscribed);
227    }
228
229    @Override
230    public String toString() {
231        StringBuilder buf = new StringBuilder();
232        if (getName() != null) {
233            buf.append(getName()).append(": ");
234        }
235        buf.append(getJid());
236        Collection<RosterGroup> groups = getGroups();
237        if (!groups.isEmpty()) {
238            buf.append(" [");
239            Iterator<RosterGroup> iter = groups.iterator();
240            RosterGroup group = iter.next();
241            buf.append(group.getName());
242            while (iter.hasNext()) {
243            buf.append(", ");
244                group = iter.next();
245                buf.append(group.getName());
246            }
247            buf.append(']');
248        }
249        return buf.toString();
250    }
251
252    @Override
253    public int hashCode() {
254        return getJid().hashCode();
255    }
256
257    @Override
258    public boolean equals(Object object) {
259        return EqualsUtil.equals(this, object, (e, o) ->
260            e.append(getJid(), o.getJid())
261        );
262    }
263
264    /**
265     * Indicates whether some other object is "equal to" this by comparing all members.
266     * <p>
267     * The {@link #equals(Object)} method returns <code>true</code> if the user JIDs are equal.
268     *
269     * @param obj the reference object with which to compare.
270     * @return <code>true</code> if this object is the same as the obj argument; <code>false</code>
271     *         otherwise.
272     */
273    public boolean equalsDeep(Object obj) {
274        return EqualsUtil.equals(this, obj, (e, o) ->
275            e.append(item, o.item)
276        );
277    }
278
279    /**
280     * Convert the RosterEntry to a Roster stanza &lt;item/&gt; element.
281     *
282     * @param entry the roster entry.
283     * @return the roster item.
284     */
285    static RosterPacket.Item toRosterItem(RosterEntry entry) {
286        return toRosterItem(entry, entry.getName(), false);
287    }
288
289    /**
290     * Convert the RosterEntry to a Roster stanza &lt;item/&gt; element.
291     *
292     * @param entry the roster entry
293     * @param name the name of the roster item.
294     * @return the roster item.
295     */
296    static RosterPacket.Item toRosterItem(RosterEntry entry, String name) {
297        return toRosterItem(entry, name, false);
298    }
299
300    static RosterPacket.Item toRosterItem(RosterEntry entry, boolean includeAskAttribute) {
301        return toRosterItem(entry, entry.getName(), includeAskAttribute);
302    }
303
304    /**
305     * Convert a roster entry with the given name to a roster item. As per RFC 6121 ยง 2.1.2.2., clients MUST NOT include
306     * the 'ask' attribute, thus set {@code includeAskAttribute} to {@code false}.
307     *
308     * @param entry the roster entry.
309     * @param name the name of the roster item.
310     * @param includeAskAttribute whether or not to include the 'ask' attribute.
311     * @return the roster item.
312     */
313    private static RosterPacket.Item toRosterItem(RosterEntry entry, String name, boolean includeAskAttribute) {
314        RosterPacket.Item item = new RosterPacket.Item(entry.getJid(), name);
315        item.setItemType(entry.getType());
316        if (includeAskAttribute) {
317            item.setSubscriptionPending(entry.isSubscriptionPending());
318        }
319        item.setApproved(entry.isApproved());
320        // Set the correct group names for the item.
321        for (RosterGroup group : entry.getGroups()) {
322            item.addGroupName(group.getName());
323        }
324        return item;
325    }
326
327}