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