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