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