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