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