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