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