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