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