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 */
017package org.jivesoftware.smackx.disco.packet;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.HashSet;
023import java.util.LinkedList;
024import java.util.List;
025import java.util.Set;
026
027import org.jivesoftware.smack.packet.IQ;
028import org.jivesoftware.smack.util.StringUtils;
029import org.jivesoftware.smack.util.TypedCloneable;
030import org.jivesoftware.smack.util.XmlStringBuilder;
031
032import org.jxmpp.util.XmppStringUtils;
033
034/**
035 * A DiscoverInfo IQ packet, which is used by XMPP clients to request and receive information
036 * to/from other XMPP entities.<p>
037 *
038 * The received information may contain one or more identities of the requested XMPP entity, and
039 * a list of supported features by the requested XMPP entity.
040 *
041 * @author Gaston Dombiak
042 */
043public class DiscoverInfo extends IQ implements TypedCloneable<DiscoverInfo> {
044
045    public static final String ELEMENT = QUERY_ELEMENT;
046    public static final String NAMESPACE = "http://jabber.org/protocol/disco#info";
047
048    private final List<Feature> features = new LinkedList<>();
049    private final Set<Feature> featuresSet = new HashSet<>();
050    private final List<Identity> identities = new LinkedList<>();
051    private final Set<String> identitiesSet = new HashSet<>();
052    private String node;
053    private boolean containsDuplicateFeatures;
054
055    public DiscoverInfo() {
056        super(ELEMENT, NAMESPACE);
057    }
058
059    /**
060     * Copy constructor.
061     *
062     * @param d
063     */
064    public DiscoverInfo(DiscoverInfo d) {
065        super(d);
066
067        // Set node
068        setNode(d.getNode());
069
070        // Copy features
071        for (Feature f : d.features) {
072            addFeature(f.clone());
073        }
074
075        // Copy identities
076        for (Identity i : d.identities) {
077            addIdentity(i.clone());
078        }
079    }
080
081    /**
082     * Adds a new feature to the discovered information.
083     *
084     * @param feature the discovered feature
085     * @return true if the feature did not already exist.
086     */
087    public boolean addFeature(String feature) {
088        return addFeature(new Feature(feature));
089    }
090
091    /**
092     * Adds a collection of features to the packet. Does noting if featuresToAdd is null.
093     *
094     * @param featuresToAdd
095     */
096    public void addFeatures(Collection<String> featuresToAdd) {
097        if (featuresToAdd == null) return;
098        for (String feature : featuresToAdd) {
099            addFeature(feature);
100        }
101    }
102
103    public boolean addFeature(Feature feature) {
104        features.add(feature);
105        boolean featureIsNew = featuresSet.add(feature);
106        if (!featureIsNew) {
107            containsDuplicateFeatures = true;
108        }
109        return featureIsNew;
110    }
111
112    /**
113     * Returns the discovered features of an XMPP entity.
114     *
115     * @return an unmodifiable list of the discovered features of an XMPP entity
116     */
117    public List<Feature> getFeatures() {
118        return Collections.unmodifiableList(features);
119    }
120
121    /**
122     * Adds a new identity of the requested entity to the discovered information.
123     *
124     * @param identity the discovered entity's identity
125     */
126    public void addIdentity(Identity identity) {
127        identities.add(identity);
128        identitiesSet.add(identity.getKey());
129    }
130
131    /**
132     * Adds identities to the DiscoverInfo stanza.
133     *
134     * @param identitiesToAdd
135     */
136    public void addIdentities(Collection<Identity> identitiesToAdd) {
137        if (identitiesToAdd == null) return;
138        for (Identity identity : identitiesToAdd) {
139            addIdentity(identity);
140        }
141    }
142
143    /**
144     * Returns the discovered identities of an XMPP entity.
145     *
146     * @return an unmodifiable list of the discovered identities
147     */
148    public List<Identity> getIdentities() {
149        return Collections.unmodifiableList(identities);
150    }
151
152    /**
153     * Returns true if this DiscoverInfo contains at least one Identity of the given category and type.
154     *
155     * @param category the category to look for.
156     * @param type the type to look for.
157     * @return true if this DiscoverInfo contains a Identity of the given category and type.
158     */
159    public boolean hasIdentity(String category, String type) {
160        String key = XmppStringUtils.generateKey(category, type);
161        return identitiesSet.contains(key);
162    }
163
164    /**
165     * Returns all Identities of the given category and type of this DiscoverInfo.
166     *
167     * @param category category the category to look for.
168     * @param type type the type to look for.
169     * @return a list of Identites with the given category and type.
170     */
171    public List<Identity> getIdentities(String category, String type) {
172        List<Identity> res = new ArrayList<>(identities.size());
173        for (Identity identity : identities) {
174            if (identity.getCategory().equals(category) && identity.getType().equals(type)) {
175                res.add(identity);
176            }
177        }
178        return res;
179    }
180
181    /**
182     * Returns the node attribute that supplements the 'jid' attribute. A node is merely
183     * something that is associated with a JID and for which the JID can provide information.<p>
184     *
185     * Node attributes SHOULD be used only when trying to provide or query information which
186     * is not directly addressable.
187     *
188     * @return the node attribute that supplements the 'jid' attribute
189     */
190    public String getNode() {
191        return node;
192    }
193
194    /**
195     * Sets the node attribute that supplements the 'jid' attribute. A node is merely
196     * something that is associated with a JID and for which the JID can provide information.<p>
197     *
198     * Node attributes SHOULD be used only when trying to provide or query information which
199     * is not directly addressable.
200     *
201     * @param node the node attribute that supplements the 'jid' attribute
202     */
203    public void setNode(String node) {
204        this.node = node;
205    }
206
207    /**
208     * Returns true if the specified feature is part of the discovered information.
209     *
210     * @param feature the feature to check
211     * @return true if the requests feature has been discovered
212     */
213    public boolean containsFeature(CharSequence feature) {
214        return features.contains(new Feature(feature));
215    }
216
217    @Override
218    protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) {
219        xml.optAttribute("node", getNode());
220        xml.rightAngleBracket();
221        for (Identity identity : identities) {
222            xml.append(identity.toXML());
223        }
224        for (Feature feature : features) {
225            xml.append(feature.toXML());
226        }
227
228        return xml;
229    }
230
231    /**
232     * Test if a DiscoverInfo response contains duplicate identities.
233     *
234     * @return true if duplicate identities where found, otherwise false
235     */
236    public boolean containsDuplicateIdentities() {
237        List<Identity> checkedIdentities = new LinkedList<>();
238        for (Identity i : identities) {
239            for (Identity i2 : checkedIdentities) {
240                if (i.equals(i2))
241                    return true;
242            }
243            checkedIdentities.add(i);
244        }
245        return false;
246    }
247
248    /**
249     * Test if a DiscoverInfo response contains duplicate features.
250     *
251     * @return true if duplicate identities where found, otherwise false
252     */
253    public boolean containsDuplicateFeatures() {
254        return containsDuplicateFeatures;
255    }
256
257    @Override
258    public DiscoverInfo clone() {
259        return new DiscoverInfo(this);
260    }
261
262    /**
263     * Represents the identity of a given XMPP entity. An entity may have many identities but all
264     * the identities SHOULD have the same name.<p>
265     *
266     * Refer to <a href="https://xmpp.org/registrar/disco-categories.html">XMPP Registry for Service Discovery Identities</a>
267     * in order to get the official registry of values for the <i>category</i> and <i>type</i>
268     * attributes.
269     *
270     */
271    public static class Identity implements Comparable<Identity>, TypedCloneable<Identity> {
272
273        private final String category;
274        private final String type;
275        private final String key;
276        private final String name;
277        private final String lang; // 'xml:lang;
278
279        public Identity(Identity identity) {
280            this.category = identity.category;
281            this.type = identity.type;
282            this.key = identity.type;
283            this.name = identity.name;
284            this.lang = identity.lang;
285        }
286
287        /**
288         * Creates a new identity for an XMPP entity.
289         *
290         * @param category the entity's category (required as per XEP-30).
291         * @param type the entity's type (required as per XEP-30).
292         */
293        public Identity(String category, String type) {
294            this(category, type, null, null);
295        }
296
297        /**
298         * Creates a new identity for an XMPP entity.
299         * 'category' and 'type' are required by
300         * <a href="http://xmpp.org/extensions/xep-0030.html#schemas">XEP-30 XML Schemas</a>
301         *
302         * @param category the entity's category (required as per XEP-30).
303         * @param name the entity's name.
304         * @param type the entity's type (required as per XEP-30).
305         */
306        public Identity(String category, String name, String type) {
307            this(category, type, name, null);
308        }
309
310        /**
311         * Creates a new identity for an XMPP entity.
312         * 'category' and 'type' are required by
313         * <a href="http://xmpp.org/extensions/xep-0030.html#schemas">XEP-30 XML Schemas</a>
314         *
315         * @param category the entity's category (required as per XEP-30).
316         * @param type the entity's type (required as per XEP-30).
317         * @param name the entity's name.
318         * @param lang the entity's lang.
319         */
320        public Identity(String category, String type, String name, String lang) {
321            this.category = StringUtils.requireNotNullOrEmpty(category, "category cannot be null");
322            this.type = StringUtils.requireNotNullOrEmpty(type, "type cannot be null");
323            this.key = XmppStringUtils.generateKey(category, type);
324            this.name = name;
325            this.lang = lang;
326        }
327
328        /**
329         * Returns the entity's category. To get the official registry of values for the
330         * 'category' attribute refer to <a href="https://xmpp.org/registrar/disco-categories.html">XMPP Registry for Service Discovery Identities</a>.
331         *
332         * @return the entity's category.
333         */
334        public String getCategory() {
335            return category;
336        }
337
338        /**
339         * Returns the identity's name.
340         *
341         * @return the identity's name.
342         */
343        public String getName() {
344            return name;
345        }
346
347        /**
348         * Returns the entity's type. To get the official registry of values for the
349         * 'type' attribute refer to <a href="https://xmpp.org/registrar/disco-categories.html">XMPP Registry for Service Discovery Identities</a>.
350         *
351         * @return the entity's type.
352         */
353        public String getType() {
354            return type;
355        }
356
357        /**
358         * Returns the identities natural language if one is set.
359         *
360         * @return the value of xml:lang of this Identity
361         */
362        public String getLanguage() {
363            return lang;
364        }
365
366        private String getKey() {
367            return key;
368        }
369
370        /**
371         * Returns true if this identity is of the given category and type.
372         *
373         * @param category the category.
374         * @param type the type.
375         * @return true if this identity is of the given category and type.
376         */
377        public boolean isOfCategoryAndType(String category, String type) {
378            return this.category.equals(category) && this.type.equals(type);
379        }
380
381        public XmlStringBuilder toXML() {
382            XmlStringBuilder xml = new XmlStringBuilder();
383            xml.halfOpenElement("identity");
384            xml.xmllangAttribute(lang);
385            xml.attribute("category", category);
386            xml.optAttribute("name", name);
387            xml.optAttribute("type", type);
388            xml.closeEmptyElement();
389            return xml;
390        }
391
392        /**
393         * Check equality for Identity  for category, type, lang and name
394         * in that order as defined by
395         * <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0015 5.4 Processing Method (Step 3.3)</a>.
396         *
397         */
398        @Override
399        public boolean equals(Object obj) {
400            if (obj == null)
401                return false;
402            if (obj == this)
403                return true;
404            if (obj.getClass() != getClass())
405                return false;
406
407            DiscoverInfo.Identity other = (DiscoverInfo.Identity) obj;
408            if (!this.key.equals(other.key))
409                return false;
410
411            String otherLang = other.lang == null ? "" : other.lang;
412            String thisLang = lang == null ? "" : lang;
413            if (!otherLang.equals(thisLang))
414                return false;
415
416            String otherName = other.name == null ? "" : other.name;
417            String thisName = name == null ? "" : other.name;
418            if (!thisName.equals(otherName))
419                return false;
420
421            return true;
422        }
423
424        @Override
425        public int hashCode() {
426            int result = 1;
427            result = 37 * result + key.hashCode();
428            result = 37 * result + (lang == null ? 0 : lang.hashCode());
429            result = 37 * result + (name == null ? 0 : name.hashCode());
430            return result;
431        }
432
433        /**
434         * Compares this identity with another one. The comparison order is: Category, Type, Lang.
435         * If all three are identical the other Identity is considered equal. Name is not used for
436         * comparison, as defined by XEP-0115
437         *
438         * @param other
439         * @return a negative integer, zero, or a positive integer as this object is less than,
440         *         equal to, or greater than the specified object.
441         */
442        @Override
443        public int compareTo(DiscoverInfo.Identity other) {
444            String otherLang = other.lang == null ? "" : other.lang;
445            String thisLang = lang == null ? "" : lang;
446
447            // This can be removed once the deprecated constructor is removed.
448            String otherType = other.type == null ? "" : other.type;
449            String thisType = type == null ? "" : type;
450
451            if (category.equals(other.category)) {
452                if (thisType.equals(otherType)) {
453                    if (thisLang.equals(otherLang)) {
454                        // Don't compare on name, XEP-30 says that name SHOULD
455                        // be equals for all identities of an entity
456                        return 0;
457                    } else {
458                        return thisLang.compareTo(otherLang);
459                    }
460                } else {
461                    return thisType.compareTo(otherType);
462                }
463            } else {
464                return category.compareTo(other.category);
465            }
466        }
467
468        @Override
469        public Identity clone() {
470            return new Identity(this);
471        }
472
473        @Override
474        public String toString() {
475            return toXML().toString();
476        }
477    }
478
479    /**
480     * Represents the features offered by the item. This information helps the requester to determine
481     * what actions are possible with regard to this item (registration, search, join, etc.)
482     * as well as specific feature types of interest, if any (e.g., for the purpose of feature
483     * negotiation).
484     */
485    public static class Feature implements TypedCloneable<Feature> {
486
487        private final String variable;
488
489        public Feature(Feature feature) {
490            this.variable = feature.variable;
491        }
492
493        public Feature(CharSequence variable) {
494            this(variable.toString());
495        }
496
497        /**
498         * Creates a new feature offered by an XMPP entity or item.
499         *
500         * @param variable the feature's variable.
501         */
502        public Feature(String variable) {
503            this.variable = StringUtils.requireNotNullOrEmpty(variable, "variable cannot be null");
504        }
505
506        /**
507         * Returns the feature's variable.
508         *
509         * @return the feature's variable.
510         */
511        public String getVar() {
512            return variable;
513        }
514
515        public XmlStringBuilder toXML() {
516            XmlStringBuilder xml = new XmlStringBuilder();
517            xml.halfOpenElement("feature");
518            xml.attribute("var", variable);
519            xml.closeEmptyElement();
520            return xml;
521        }
522
523        @Override
524        public boolean equals(Object obj) {
525            if (obj == null)
526                return false;
527            if (obj == this)
528                return true;
529            if (obj.getClass() != getClass())
530                return false;
531
532            DiscoverInfo.Feature other = (DiscoverInfo.Feature) obj;
533            return variable.equals(other.variable);
534        }
535
536        @Override
537        public int hashCode() {
538            return variable.hashCode();
539        }
540
541        @Override
542        public Feature clone() {
543            return new Feature(this);
544        }
545
546        @Override
547        public String toString() {
548            return toXML().toString();
549        }
550    }
551}