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