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