1 /** 2 * $RCSfile$ 3 * $Revision$ 4 * $Date$ 5 * 6 * Copyright 2003-2007 Jive Software. 7 * 8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); 9 * you may not use this file except in compliance with the License. 10 * You may obtain a copy of the License at 11 * 12 * http://www.apache.org/licenses/LICENSE-2.0 13 * 14 * Unless required by applicable law or agreed to in writing, software 15 * distributed under the License is distributed on an "AS IS" BASIS, 16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 * See the License for the specific language governing permissions and 18 * limitations under the License. 19 */ 20 21 package org.jivesoftware.smackx.packet; 22 23 import org.jivesoftware.smack.packet.IQ; 24 import org.jivesoftware.smack.util.StringUtils; 25 26 import java.util.Collection; 27 import java.util.Collections; 28 import java.util.Iterator; 29 import java.util.LinkedList; 30 import java.util.List; 31 import java.util.concurrent.CopyOnWriteArrayList; 32 33 /** 34 * A DiscoverInfo IQ packet, which is used by XMPP clients to request and receive information 35 * to/from other XMPP entities.<p> 36 * 37 * The received information may contain one or more identities of the requested XMPP entity, and 38 * a list of supported features by the requested XMPP entity. 39 * 40 * @author Gaston Dombiak 41 */ 42 public class DiscoverInfo extends IQ { 43 44 public static final String NAMESPACE = "http://jabber.org/protocol/disco#info"; 45 46 private final List<Feature> features = new CopyOnWriteArrayList<Feature>(); 47 private final List<Identity> identities = new CopyOnWriteArrayList<Identity>(); 48 private String node; 49 50 public DiscoverInfo() { 51 super(); 52 } 53 54 /** 55 * Copy constructor 56 * 57 * @param d 58 */ 59 public DiscoverInfo(DiscoverInfo d) { 60 super(d); 61 62 // Set node 63 setNode(d.getNode()); 64 65 // Copy features 66 synchronized (d.features) { 67 for (Feature f : d.features) { 68 addFeature(f); 69 } 70 } 71 72 // Copy identities 73 synchronized (d.identities) { 74 for (Identity i : d.identities) { 75 addIdentity(i); 76 } 77 } 78 } 79 80 /** 81 * Adds a new feature to the discovered information. 82 * 83 * @param feature the discovered feature 84 */ 85 public void addFeature(String feature) { 86 addFeature(new Feature(feature)); 87 } 88 89 /** 90 * Adds a collection of features to the packet. Does noting if featuresToAdd is null. 91 * 92 * @param featuresToAdd 93 */ 94 public void addFeatures(Collection<String> featuresToAdd) { 95 if (featuresToAdd == null) return; 96 for (String feature : featuresToAdd) { 97 addFeature(feature); 98 } 99 } 100 101 private void addFeature(Feature feature) { 102 synchronized (features) { 103 features.add(feature); 104 } 105 } 106 107 /** 108 * Returns the discovered features of an XMPP entity. 109 * 110 * @return an Iterator on the discovered features of an XMPP entity 111 */ 112 public Iterator<Feature> getFeatures() { 113 synchronized (features) { 114 return Collections.unmodifiableList(features).iterator(); 115 } 116 } 117 118 /** 119 * Adds a new identity of the requested entity to the discovered information. 120 * 121 * @param identity the discovered entity's identity 122 */ 123 public void addIdentity(Identity identity) { 124 synchronized (identities) { 125 identities.add(identity); 126 } 127 } 128 129 /** 130 * Adds identities to the DiscoverInfo stanza 131 * 132 * @param identitiesToAdd 133 */ 134 public void addIdentities(Collection<Identity> identitiesToAdd) { 135 if (identitiesToAdd == null) return; 136 synchronized (identities) { 137 identities.addAll(identitiesToAdd); 138 } 139 } 140 141 /** 142 * Returns the discovered identities of an XMPP entity. 143 * 144 * @return an Iterator on the discoveted identities 145 */ 146 public Iterator<Identity> getIdentities() { 147 synchronized (identities) { 148 return Collections.unmodifiableList(identities).iterator(); 149 } 150 } 151 152 /** 153 * Returns the node attribute that supplements the 'jid' attribute. A node is merely 154 * something that is associated with a JID and for which the JID can provide information.<p> 155 * 156 * Node attributes SHOULD be used only when trying to provide or query information which 157 * is not directly addressable. 158 * 159 * @return the node attribute that supplements the 'jid' attribute 160 */ 161 public String getNode() { 162 return node; 163 } 164 165 /** 166 * Sets the node attribute that supplements the 'jid' attribute. A node is merely 167 * something that is associated with a JID and for which the JID can provide information.<p> 168 * 169 * Node attributes SHOULD be used only when trying to provide or query information which 170 * is not directly addressable. 171 * 172 * @param node the node attribute that supplements the 'jid' attribute 173 */ 174 public void setNode(String node) { 175 this.node = node; 176 } 177 178 /** 179 * Returns true if the specified feature is part of the discovered information. 180 * 181 * @param feature the feature to check 182 * @return true if the requestes feature has been discovered 183 */ 184 public boolean containsFeature(String feature) { 185 for (Iterator<Feature> it = getFeatures(); it.hasNext();) { 186 if (feature.equals(it.next().getVar())) 187 return true; 188 } 189 return false; 190 } 191 192 public String getChildElementXML() { 193 StringBuilder buf = new StringBuilder(); 194 buf.append("<query xmlns=\"" + NAMESPACE + "\""); 195 if (getNode() != null) { 196 buf.append(" node=\""); 197 buf.append(StringUtils.escapeForXML(getNode())); 198 buf.append("\""); 199 } 200 buf.append(">"); 201 synchronized (identities) { 202 for (Identity identity : identities) { 203 buf.append(identity.toXML()); 204 } 205 } 206 synchronized (features) { 207 for (Feature feature : features) { 208 buf.append(feature.toXML()); 209 } 210 } 211 // Add packet extensions, if any are defined. 212 buf.append(getExtensionsXML()); 213 buf.append("</query>"); 214 return buf.toString(); 215 } 216 217 /** 218 * Test if a DiscoverInfo response contains duplicate identities. 219 * 220 * @return true if duplicate identities where found, otherwise false 221 */ 222 public boolean containsDuplicateIdentities() { 223 List<Identity> checkedIdentities = new LinkedList<Identity>(); 224 for (Identity i : identities) { 225 for (Identity i2 : checkedIdentities) { 226 if (i.equals(i2)) 227 return true; 228 } 229 checkedIdentities.add(i); 230 } 231 return false; 232 } 233 234 /** 235 * Test if a DiscoverInfo response contains duplicate features. 236 * 237 * @return true if duplicate identities where found, otherwise false 238 */ 239 public boolean containsDuplicateFeatures() { 240 List<Feature> checkedFeatures = new LinkedList<Feature>(); 241 for (Feature f : features) { 242 for (Feature f2 : checkedFeatures) { 243 if (f.equals(f2)) 244 return true; 245 } 246 checkedFeatures.add(f); 247 } 248 return false; 249 } 250 251 /** 252 * Represents the identity of a given XMPP entity. An entity may have many identities but all 253 * the identities SHOULD have the same name.<p> 254 * 255 * Refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> 256 * in order to get the official registry of values for the <i>category</i> and <i>type</i> 257 * attributes. 258 * 259 */ 260 public static class Identity implements Comparable<Identity> { 261 262 private String category; 263 private String name; 264 private String type; 265 private String lang; // 'xml:lang; 266 267 /** 268 * Creates a new identity for an XMPP entity. 269 * 270 * @param category the entity's category. 271 * @param name the entity's name. 272 * @deprecated As per the spec, the type field is mandatory and the 3 argument constructor should be used instead. 273 */ 274 public Identity(String category, String name) { 275 this.category = category; 276 this.name = name; 277 } 278 279 /** 280 * Creates a new identity for an XMPP entity. 281 * 'category' and 'type' are required by 282 * <a href="http://xmpp.org/extensions/xep-0030.html#schemas">XEP-30 XML Schemas</a> 283 * 284 * @param category the entity's category (required as per XEP-30). 285 * @param name the entity's name. 286 * @param type the entity's type (required as per XEP-30). 287 */ 288 public Identity(String category, String name, String type) { 289 if ((category == null) || (type == null)) 290 throw new IllegalArgumentException("category and type cannot be null"); 291 292 this.category = category; 293 this.name = name; 294 this.type = type; 295 } 296 297 /** 298 * Returns the entity's category. To get the official registry of values for the 299 * 'category' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> 300 * 301 * @return the entity's category. 302 */ 303 public String getCategory() { 304 return category; 305 } 306 307 /** 308 * Returns the identity's name. 309 * 310 * @return the identity's name. 311 */ 312 public String getName() { 313 return name; 314 } 315 316 /** 317 * Returns the entity's type. To get the official registry of values for the 318 * 'type' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> 319 * 320 * @return the entity's type. 321 */ 322 public String getType() { 323 return type; 324 } 325 326 /** 327 * Sets the entity's type. To get the official registry of values for the 328 * 'type' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> 329 * 330 * @param type the identity's type. 331 * @deprecated As per the spec, this field is mandatory and the 3 argument constructor should be used instead. 332 */ 333 public void setType(String type) { 334 this.type = type; 335 } 336 337 /** 338 * Sets the natural language (xml:lang) for this identity (optional) 339 * 340 * @param lang the xml:lang of this Identity 341 */ 342 public void setLanguage(String lang) { 343 this.lang = lang; 344 } 345 346 /** 347 * Returns the identities natural language if one is set 348 * 349 * @return the value of xml:lang of this Identity 350 */ 351 public String getLanguage() { 352 return lang; 353 } 354 355 public String toXML() { 356 StringBuilder buf = new StringBuilder(); 357 buf.append("<identity"); 358 // Check if this packet has 'lang' set and maybe append it to the resulting string 359 if (lang != null) 360 buf.append(" xml:lang=\"").append(StringUtils.escapeForXML(lang)).append("\""); 361 // Category must always be set 362 buf.append(" category=\"").append(StringUtils.escapeForXML(category)).append("\""); 363 // Name must always be set 364 buf.append(" name=\"").append(StringUtils.escapeForXML(name)).append("\""); 365 // Check if this packet has 'type' set and maybe append it to the resulting string 366 if (type != null) { 367 buf.append(" type=\"").append(StringUtils.escapeForXML(type)).append("\""); 368 } 369 buf.append("/>"); 370 return buf.toString(); 371 } 372 373 /** 374 * Check equality for Identity for category, type, lang and name 375 * in that order as defined by 376 * <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0015 5.4 Processing Method (Step 3.3)</a> 377 * 378 */ 379 public boolean equals(Object obj) { 380 if (obj == null) 381 return false; 382 if (obj == this) 383 return true; 384 if (obj.getClass() != getClass()) 385 return false; 386 387 DiscoverInfo.Identity other = (DiscoverInfo.Identity) obj; 388 if (!this.category.equals(other.category)) 389 return false; 390 391 String otherLang = other.lang == null ? "" : other.lang; 392 String thisLang = lang == null ? "" : lang; 393 if (!otherLang.equals(thisLang)) 394 return false; 395 396 // This safeguard can be removed once the deprecated constructor is removed. 397 String otherType = other.type == null ? "" : other.type; 398 String thisType = type == null ? "" : type; 399 if (!otherType.equals(thisType)) 400 return false; 401 402 String otherName = other.name == null ? "" : other.name; 403 String thisName = name == null ? "" : other.name; 404 if (!thisName.equals(otherName)) 405 return false; 406 407 return true; 408 } 409 410 @Override 411 public int hashCode() { 412 int result = 1; 413 result = 37 * result + category.hashCode(); 414 result = 37 * result + (lang == null ? 0 : lang.hashCode()); 415 result = 37 * result + (type == null ? 0 : type.hashCode()); 416 result = 37 * result + (name == null ? 0 : name.hashCode()); 417 return result; 418 } 419 420 /** 421 * Compares this identity with another one. The comparison order is: 422 * Category, Type, Lang. If all three are identical the other Identity is considered equal. 423 * Name is not used for comparision, as defined by XEP-0115 424 * 425 * @param obj 426 * @return 427 */ 428 public int compareTo(DiscoverInfo.Identity other) { 429 String otherLang = other.lang == null ? "" : other.lang; 430 String thisLang = lang == null ? "" : lang; 431 432 // This can be removed once the deprecated constructor is removed. 433 String otherType = other.type == null ? "" : other.type; 434 String thisType = type == null ? "" : type; 435 436 if (category.equals(other.category)) { 437 if (thisType.equals(otherType)) { 438 if (thisLang.equals(otherLang)) { 439 // Don't compare on name, XEP-30 says that name SHOULD 440 // be equals for all identities of an entity 441 return 0; 442 } else { 443 return thisLang.compareTo(otherLang); 444 } 445 } else { 446 return thisType.compareTo(otherType); 447 } 448 } else { 449 return category.compareTo(other.category); 450 } 451 } 452 } 453 454 /** 455 * Represents the features offered by the item. This information helps requestors determine 456 * what actions are possible with regard to this item (registration, search, join, etc.) 457 * as well as specific feature types of interest, if any (e.g., for the purpose of feature 458 * negotiation). 459 */ 460 public static class Feature { 461 462 private String variable; 463 464 /** 465 * Creates a new feature offered by an XMPP entity or item. 466 * 467 * @param variable the feature's variable. 468 */ 469 public Feature(String variable) { 470 if (variable == null) 471 throw new IllegalArgumentException("variable cannot be null"); 472 this.variable = variable; 473 } 474 475 /** 476 * Returns the feature's variable. 477 * 478 * @return the feature's variable. 479 */ 480 public String getVar() { 481 return variable; 482 } 483 484 public String toXML() { 485 StringBuilder buf = new StringBuilder(); 486 buf.append("<feature var=\"").append(StringUtils.escapeForXML(variable)).append("\"/>"); 487 return buf.toString(); 488 } 489 490 public boolean equals(Object obj) { 491 if (obj == null) 492 return false; 493 if (obj == this) 494 return true; 495 if (obj.getClass() != getClass()) 496 return false; 497 498 DiscoverInfo.Feature other = (DiscoverInfo.Feature) obj; 499 return variable.equals(other.variable); 500 } 501 502 @Override 503 public int hashCode() { 504 return 37 * variable.hashCode(); 505 } 506 } 507 } 508