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.packet;
019
020import java.util.ArrayList;
021import java.util.Collections;
022import java.util.List;
023import java.util.Locale;
024import java.util.Set;
025import java.util.concurrent.CopyOnWriteArraySet;
026
027import org.jivesoftware.smack.packet.ExtensionElement;
028import org.jivesoftware.smack.packet.IQ;
029import org.jivesoftware.smack.packet.Stanza;
030import org.jivesoftware.smack.util.EqualsUtil;
031import org.jivesoftware.smack.util.HashCode;
032import org.jivesoftware.smack.util.Objects;
033import org.jivesoftware.smack.util.StringUtils;
034import org.jivesoftware.smack.util.XmlStringBuilder;
035
036import org.jxmpp.jid.BareJid;
037
038/**
039 * Represents XMPP roster packets.
040 *
041 * @author Matt Tucker
042 * @author Florian Schmaus
043 */
044public final class RosterPacket extends IQ {
045
046    public static final String ELEMENT = QUERY_ELEMENT;
047    public static final String NAMESPACE = "jabber:iq:roster";
048
049    private final List<Item> rosterItems = new ArrayList<>();
050    private String rosterVersion;
051
052    public RosterPacket() {
053        super(ELEMENT, NAMESPACE);
054    }
055
056    /**
057     * Adds a roster item to the packet.
058     *
059     * @param item a roster item.
060     */
061    public void addRosterItem(Item item) {
062        synchronized (rosterItems) {
063            rosterItems.add(item);
064        }
065    }
066
067    /**
068     * Returns the number of roster items in this roster packet.
069     *
070     * @return the number of roster items.
071     */
072    public int getRosterItemCount() {
073        synchronized (rosterItems) {
074            return rosterItems.size();
075        }
076    }
077
078    /**
079     * Returns a copied list of the roster items in the packet.
080     *
081     * @return a copied list of the roster items in the packet.
082     */
083    public List<Item> getRosterItems() {
084        synchronized (rosterItems) {
085            return new ArrayList<>(rosterItems);
086        }
087    }
088
089    @Override
090    protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder buf) {
091        buf.optAttribute("ver", rosterVersion);
092        buf.rightAngleBracket();
093
094        synchronized (rosterItems) {
095            for (Item entry : rosterItems) {
096                buf.append(entry.toXML());
097            }
098        }
099        return buf;
100    }
101
102    public String getVersion() {
103        return rosterVersion;
104    }
105
106    public void setVersion(String version) {
107        rosterVersion = version;
108    }
109
110    /**
111     * A roster item, which consists of a JID, their name, the type of subscription, and
112     * the groups the roster item belongs to.
113     */
114    // TODO Make this class immutable.
115    public static final class Item implements ExtensionElement {
116
117        /**
118         * The constant value "{@value}".
119         */
120        public static final String ELEMENT = Stanza.ITEM;
121
122        public static final String GROUP = "group";
123
124        private final BareJid jid;
125
126        /**
127         * TODO describe me. With link to the RFC. Is ask= attribute.
128         */
129        private boolean subscriptionPending;
130
131        // TODO Make immutable.
132        private String name;
133        private ItemType itemType = ItemType.none;
134        private boolean approved;
135        private final Set<String> groupNames;
136
137        /**
138         * Creates a new roster item.
139         *
140         * @param jid TODO javadoc me please
141         * @param name TODO javadoc me please
142         */
143        public Item(BareJid jid, String name) {
144            this(jid, name, false);
145        }
146
147        /**
148         * Creates a new roster item.
149         *
150         * @param jid the jid.
151         * @param name the user's name.
152         * @param subscriptionPending TODO javadoc me please
153         */
154        public Item(BareJid jid, String name, boolean subscriptionPending) {
155            this.jid = Objects.requireNonNull(jid);
156            this.name = name;
157            this.subscriptionPending = subscriptionPending;
158            groupNames = new CopyOnWriteArraySet<>();
159        }
160
161        @Override
162        public String getElementName() {
163            return ELEMENT;
164        }
165
166        @Override
167        public String getNamespace() {
168            return NAMESPACE;
169        }
170
171        /**
172         * Returns the user.
173         *
174         * @return the user.
175         * @deprecated use {@link #getJid()} instead.
176         */
177        @Deprecated
178        public String getUser() {
179            return jid.toString();
180        }
181
182        /**
183         * Returns the JID of this item.
184         *
185         * @return the JID.
186         */
187        public BareJid getJid() {
188            return jid;
189        }
190
191        /**
192         * Returns the user's name.
193         *
194         * @return the user's name.
195         */
196        public String getName() {
197            return name;
198        }
199
200        /**
201         * Sets the user's name.
202         *
203         * @param name the user's name.
204         */
205        public void setName(String name) {
206            this.name = name;
207        }
208
209        /**
210         * Returns the roster item type.
211         *
212         * @return the roster item type.
213         */
214        public ItemType getItemType() {
215            return itemType;
216        }
217
218        /**
219         * Sets the roster item type.
220         *
221         * @param itemType the roster item type.
222         */
223        public void setItemType(ItemType itemType) {
224            this.itemType = Objects.requireNonNull(itemType, "itemType must not be null");
225        }
226
227        public void setSubscriptionPending(boolean subscriptionPending) {
228            this.subscriptionPending = subscriptionPending;
229        }
230
231        public boolean isSubscriptionPending() {
232            return subscriptionPending;
233        }
234
235        /**
236         * Returns the roster item pre-approval state.
237         *
238         * @return the pre-approval state.
239         */
240        public boolean isApproved() {
241            return approved;
242        }
243
244        /**
245         * Sets the roster item pre-approval state.
246         *
247         * @param approved the pre-approval flag.
248         */
249        public void setApproved(boolean approved) {
250            this.approved = approved;
251        }
252
253        /**
254         * Returns an unmodifiable set of the group names that the roster item
255         * belongs to.
256         *
257         * @return an unmodifiable set of the group names.
258         */
259        public Set<String> getGroupNames() {
260            return Collections.unmodifiableSet(groupNames);
261        }
262
263        /**
264         * Adds a group name.
265         *
266         * @param groupName the group name.
267         */
268        public void addGroupName(String groupName) {
269            groupNames.add(groupName);
270        }
271
272        /**
273         * Removes a group name.
274         *
275         * @param groupName the group name.
276         */
277        public void removeGroupName(String groupName) {
278            groupNames.remove(groupName);
279        }
280
281        @Override
282        public XmlStringBuilder toXML(org.jivesoftware.smack.packet.XmlEnvironment enclosingNamespace) {
283            XmlStringBuilder xml = new XmlStringBuilder(this, enclosingNamespace);
284            xml.attribute("jid", jid);
285            xml.optAttribute("name", name);
286            xml.optAttribute("subscription", itemType);
287            if (subscriptionPending) {
288                xml.append(" ask='subscribe'");
289            }
290            xml.optBooleanAttribute("approved", approved);
291            xml.rightAngleBracket();
292
293            for (String groupName : groupNames) {
294                xml.openElement(GROUP).escape(groupName).closeElement(GROUP);
295            }
296            xml.closeElement(this);
297            return xml;
298        }
299
300        @Override
301        public int hashCode() {
302            return HashCode.builder()
303                .append(groupNames)
304                .append(subscriptionPending)
305                .append(itemType)
306                .append(name)
307                .append(jid)
308                .append(approved)
309                .build();
310        }
311
312        @Override
313        public boolean equals(Object obj) {
314            return EqualsUtil.equals(this, obj, (e, o) ->
315                e.append(groupNames, o.groupNames)
316                 .append(subscriptionPending, o.subscriptionPending)
317                 .append(itemType, o.itemType)
318                 .append(name, o.name)
319                 .append(jid, o.jid)
320                 .append(approved, o.approved)
321            );
322        }
323
324    }
325
326    public enum ItemType {
327
328        /**
329         * The user does not have a subscription to the contact's presence, and the contact does not
330         * have a subscription to the user's presence; this is the default value, so if the
331         * subscription attribute is not included then the state is to be understood as "none".
332         */
333        none('⊥'),
334
335        /**
336         * The user has a subscription to the contact's presence, but the contact does not have a
337         * subscription to the user's presence.
338         */
339        to('←'),
340
341        /**
342         * The contact has a subscription to the user's presence, but the user does not have a
343         * subscription to the contact's presence.
344         */
345        from('→'),
346
347        /**
348         * The user and the contact have subscriptions to each other's presence (also called a
349         * "mutual subscription").
350         */
351        both('↔'),
352
353        /**
354         * The user wishes to stop receiving presence updates from the subscriber.
355         */
356        remove('⚡'),
357        ;
358
359
360        private static final char ME = '●';
361
362        private final String symbol;
363
364        ItemType(char secondSymbolChar) {
365            StringBuilder sb = new StringBuilder(2);
366            sb.append(ME).append(secondSymbolChar);
367            symbol = sb.toString();
368        }
369
370        public static ItemType fromString(String string) {
371            if (StringUtils.isNullOrEmpty(string)) {
372                return none;
373            }
374            return ItemType.valueOf(string.toLowerCase(Locale.US));
375        }
376
377        /**
378         * Get a String containing symbols representing the item type. The first symbol in the
379         * string is a big dot, representing the local entity. The second symbol represents the
380         * established subscription relation and is typically an arrow. The head(s) of the arrow
381         * point in the direction presence messages are sent. For example, if there is only a head
382         * pointing to the big dot, then the local user will receive presence information from the
383         * remote entity.
384         *
385         * @return the symbolic representation of this item type.
386         */
387        public String asSymbol() {
388            return symbol;
389        }
390    }
391}