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