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; 018 019import org.jivesoftware.smack.SmackException.NoResponseException; 020import org.jivesoftware.smack.SmackException.NotConnectedException; 021import org.jivesoftware.smack.XMPPConnection; 022import org.jivesoftware.smack.ConnectionCreationListener; 023import org.jivesoftware.smack.Manager; 024import org.jivesoftware.smack.XMPPConnectionRegistry; 025import org.jivesoftware.smack.XMPPException.XMPPErrorException; 026import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; 027import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; 028import org.jivesoftware.smack.packet.IQ; 029import org.jivesoftware.smack.packet.Stanza; 030import org.jivesoftware.smack.packet.ExtensionElement; 031import org.jivesoftware.smack.packet.XMPPError; 032import org.jivesoftware.smack.util.Objects; 033import org.jivesoftware.smackx.caps.EntityCapsManager; 034import org.jivesoftware.smackx.disco.packet.DiscoverInfo; 035import org.jivesoftware.smackx.disco.packet.DiscoverItems; 036import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity; 037import org.jivesoftware.smackx.xdata.packet.DataForm; 038import org.jxmpp.util.cache.Cache; 039import org.jxmpp.util.cache.ExpirationCache; 040 041import java.util.ArrayList; 042import java.util.Collections; 043import java.util.HashSet; 044import java.util.LinkedList; 045import java.util.List; 046import java.util.Map; 047import java.util.Set; 048import java.util.WeakHashMap; 049import java.util.concurrent.ConcurrentHashMap; 050import java.util.logging.Level; 051import java.util.logging.Logger; 052 053/** 054 * Manages discovery of services in XMPP entities. This class provides: 055 * <ol> 056 * <li>A registry of supported features in this XMPP entity. 057 * <li>Automatic response when this XMPP entity is queried for information. 058 * <li>Ability to discover items and information of remote XMPP entities. 059 * <li>Ability to publish publicly available items. 060 * </ol> 061 * 062 * @author Gaston Dombiak 063 */ 064public class ServiceDiscoveryManager extends Manager { 065 066 private static final Logger LOGGER = Logger.getLogger(ServiceDiscoveryManager.class.getName()); 067 068 private static final String DEFAULT_IDENTITY_NAME = "Smack"; 069 private static final String DEFAULT_IDENTITY_CATEGORY = "client"; 070 private static final String DEFAULT_IDENTITY_TYPE = "pc"; 071 072 private static DiscoverInfo.Identity defaultIdentity = new Identity(DEFAULT_IDENTITY_CATEGORY, 073 DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE); 074 075 private Set<DiscoverInfo.Identity> identities = new HashSet<DiscoverInfo.Identity>(); 076 private DiscoverInfo.Identity identity = defaultIdentity; 077 078 private EntityCapsManager capsManager; 079 080 private static Map<XMPPConnection, ServiceDiscoveryManager> instances = new WeakHashMap<>(); 081 082 private final Set<String> features = new HashSet<String>(); 083 private DataForm extendedInfo = null; 084 private Map<String, NodeInformationProvider> nodeInformationProviders = 085 new ConcurrentHashMap<String, NodeInformationProvider>(); 086 087 // Create a new ServiceDiscoveryManager on every established connection 088 static { 089 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 090 public void connectionCreated(XMPPConnection connection) { 091 getInstanceFor(connection); 092 } 093 }); 094 } 095 096 /** 097 * Set the default identity all new connections will have. If unchanged the default identity is an 098 * identity where category is set to 'client', type is set to 'pc' and name is set to 'Smack'. 099 * 100 * @param identity 101 */ 102 public static void setDefaultIdentity(DiscoverInfo.Identity identity) { 103 defaultIdentity = identity; 104 } 105 106 /** 107 * Creates a new ServiceDiscoveryManager for a given XMPPConnection. This means that the 108 * service manager will respond to any service discovery request that the connection may 109 * receive. 110 * 111 * @param connection the connection to which a ServiceDiscoveryManager is going to be created. 112 */ 113 private ServiceDiscoveryManager(XMPPConnection connection) { 114 super(connection); 115 116 addFeature(DiscoverInfo.NAMESPACE); 117 addFeature(DiscoverItems.NAMESPACE); 118 119 // Listen for disco#items requests and answer with an empty result 120 connection.registerIQRequestHandler(new AbstractIqRequestHandler(DiscoverItems.ELEMENT, DiscoverItems.NAMESPACE, IQ.Type.get, Mode.async) { 121 @Override 122 public IQ handleIQRequest(IQ iqRequest) { 123 DiscoverItems discoverItems = (DiscoverItems) iqRequest; 124 DiscoverItems response = new DiscoverItems(); 125 response.setType(IQ.Type.result); 126 response.setTo(discoverItems.getFrom()); 127 response.setStanzaId(discoverItems.getStanzaId()); 128 response.setNode(discoverItems.getNode()); 129 130 // Add the defined items related to the requested node. Look for 131 // the NodeInformationProvider associated with the requested node. 132 NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverItems.getNode()); 133 if (nodeInformationProvider != null) { 134 // Specified node was found, add node items 135 response.addItems(nodeInformationProvider.getNodeItems()); 136 // Add packet extensions 137 response.addExtensions(nodeInformationProvider.getNodePacketExtensions()); 138 } else if(discoverItems.getNode() != null) { 139 // Return <item-not-found/> error since client doesn't contain 140 // the specified node 141 response.setType(IQ.Type.error); 142 response.setError(new XMPPError(XMPPError.Condition.item_not_found)); 143 } 144 return response; 145 } 146 }); 147 148 // Listen for disco#info requests and answer the client's supported features 149 // To add a new feature as supported use the #addFeature message 150 connection.registerIQRequestHandler(new AbstractIqRequestHandler(DiscoverInfo.ELEMENT, DiscoverInfo.NAMESPACE, IQ.Type.get, Mode.async) { 151 @Override 152 public IQ handleIQRequest(IQ iqRequest) { 153 DiscoverInfo discoverInfo = (DiscoverInfo) iqRequest; 154 // Answer the client's supported features if the request is of the GET type 155 DiscoverInfo response = new DiscoverInfo(); 156 response.setType(IQ.Type.result); 157 response.setTo(discoverInfo.getFrom()); 158 response.setStanzaId(discoverInfo.getStanzaId()); 159 response.setNode(discoverInfo.getNode()); 160 // Add the client's identity and features only if "node" is null 161 // and if the request was not send to a node. If Entity Caps are 162 // enabled the client's identity and features are may also added 163 // if the right node is chosen 164 if (discoverInfo.getNode() == null) { 165 addDiscoverInfoTo(response); 166 } else { 167 // Disco#info was sent to a node. Check if we have information of the 168 // specified node 169 NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverInfo.getNode()); 170 if (nodeInformationProvider != null) { 171 // Node was found. Add node features 172 response.addFeatures(nodeInformationProvider.getNodeFeatures()); 173 // Add node identities 174 response.addIdentities(nodeInformationProvider.getNodeIdentities()); 175 // Add packet extensions 176 response.addExtensions(nodeInformationProvider.getNodePacketExtensions()); 177 } else { 178 // Return <item-not-found/> error since specified node was not found 179 response.setType(IQ.Type.error); 180 response.setError(new XMPPError(XMPPError.Condition.item_not_found)); 181 } 182 } 183 return response; 184 } 185 }); 186 } 187 188 /** 189 * Returns the name of the client that will be returned when asked for the client identity 190 * in a disco request. The name could be any value you need to identity this client. 191 * 192 * @return the name of the client that will be returned when asked for the client identity 193 * in a disco request. 194 */ 195 public String getIdentityName() { 196 return identity.getName(); 197 } 198 199 /** 200 * Sets the default identity the client will report. 201 * 202 * @param identity 203 */ 204 public synchronized void setIdentity(Identity identity) { 205 this.identity = Objects.requireNonNull(identity, "Identity can not be null"); 206 // Notify others of a state change of SDM. In order to keep the state consistent, this 207 // method is synchronized 208 renewEntityCapsVersion(); 209 } 210 211 /** 212 * Return the default identity of the client. 213 * 214 * @return the default identity. 215 */ 216 public Identity getIdentity() { 217 return identity; 218 } 219 220 /** 221 * Returns the type of client that will be returned when asked for the client identity in a 222 * disco request. The valid types are defined by the category client. Follow this link to learn 223 * the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>. 224 * 225 * @return the type of client that will be returned when asked for the client identity in a 226 * disco request. 227 */ 228 public String getIdentityType() { 229 return identity.getType(); 230 } 231 232 /** 233 * Add an further identity to the client. 234 * 235 * @param identity 236 */ 237 public synchronized void addIdentity(DiscoverInfo.Identity identity) { 238 identities.add(identity); 239 // Notify others of a state change of SDM. In order to keep the state consistent, this 240 // method is synchronized 241 renewEntityCapsVersion(); 242 } 243 244 /** 245 * Remove an identity from the client. Note that the client needs at least one identity, the default identity, which 246 * can not be removed. 247 * 248 * @param identity 249 * @return true, if successful. Otherwise the default identity was given. 250 */ 251 public synchronized boolean removeIdentity(DiscoverInfo.Identity identity) { 252 if (identity.equals(this.identity)) return false; 253 identities.remove(identity); 254 // Notify others of a state change of SDM. In order to keep the state consistent, this 255 // method is synchronized 256 renewEntityCapsVersion(); 257 return true; 258 } 259 260 /** 261 * Returns all identities of this client as unmodifiable Collection 262 * 263 * @return all identies as set 264 */ 265 public Set<DiscoverInfo.Identity> getIdentities() { 266 Set<Identity> res = new HashSet<Identity>(identities); 267 // Add the default identity that must exist 268 res.add(defaultIdentity); 269 return Collections.unmodifiableSet(res); 270 } 271 272 /** 273 * Returns the ServiceDiscoveryManager instance associated with a given XMPPConnection. 274 * 275 * @param connection the connection used to look for the proper ServiceDiscoveryManager. 276 * @return the ServiceDiscoveryManager associated with a given XMPPConnection. 277 */ 278 public static synchronized ServiceDiscoveryManager getInstanceFor(XMPPConnection connection) { 279 ServiceDiscoveryManager sdm = instances.get(connection); 280 if (sdm == null) { 281 sdm = new ServiceDiscoveryManager(connection); 282 // Register the new instance and associate it with the connection 283 instances.put(connection, sdm); 284 } 285 return sdm; 286 } 287 288 /** 289 * Add discover info response data. 290 * 291 * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol; Example 2</a> 292 * 293 * @param response the discover info response packet 294 */ 295 public synchronized void addDiscoverInfoTo(DiscoverInfo response) { 296 // First add the identities of the connection 297 response.addIdentities(getIdentities()); 298 299 // Add the registered features to the response 300 for (String feature : getFeatures()) { 301 response.addFeature(feature); 302 } 303 response.addExtension(extendedInfo); 304 } 305 306 /** 307 * Returns the NodeInformationProvider responsible for providing information 308 * (ie items) related to a given node or <tt>null</null> if none.<p> 309 * 310 * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the 311 * NodeInformationProvider will provide information about the rooms where the user has joined. 312 * 313 * @param node the node that contains items associated with an entity not addressable as a JID. 314 * @return the NodeInformationProvider responsible for providing information related 315 * to a given node. 316 */ 317 private NodeInformationProvider getNodeInformationProvider(String node) { 318 if (node == null) { 319 return null; 320 } 321 return nodeInformationProviders.get(node); 322 } 323 324 /** 325 * Sets the NodeInformationProvider responsible for providing information 326 * (ie items) related to a given node. Every time this client receives a disco request 327 * regarding the items of a given node, the provider associated to that node will be the 328 * responsible for providing the requested information.<p> 329 * 330 * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the 331 * NodeInformationProvider will provide information about the rooms where the user has joined. 332 * 333 * @param node the node whose items will be provided by the NodeInformationProvider. 334 * @param listener the NodeInformationProvider responsible for providing items related 335 * to the node. 336 */ 337 public void setNodeInformationProvider(String node, NodeInformationProvider listener) { 338 nodeInformationProviders.put(node, listener); 339 } 340 341 /** 342 * Removes the NodeInformationProvider responsible for providing information 343 * (ie items) related to a given node. This means that no more information will be 344 * available for the specified node. 345 * 346 * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the 347 * NodeInformationProvider will provide information about the rooms where the user has joined. 348 * 349 * @param node the node to remove the associated NodeInformationProvider. 350 */ 351 public void removeNodeInformationProvider(String node) { 352 nodeInformationProviders.remove(node); 353 } 354 355 /** 356 * Returns the supported features by this XMPP entity. 357 * <p> 358 * The result is a copied modifiable list of the original features. 359 * </p> 360 * 361 * @return a List of the supported features by this XMPP entity. 362 */ 363 public synchronized List<String> getFeatures() { 364 return new ArrayList<String>(features); 365 } 366 367 /** 368 * Registers that a new feature is supported by this XMPP entity. When this client is 369 * queried for its information the registered features will be answered.<p> 370 * 371 * Since no stanza(/packet) is actually sent to the server it is safe to perform this operation 372 * before logging to the server. In fact, you may want to configure the supported features 373 * before logging to the server so that the information is already available if it is required 374 * upon login. 375 * 376 * @param feature the feature to register as supported. 377 */ 378 public synchronized void addFeature(String feature) { 379 features.add(feature); 380 // Notify others of a state change of SDM. In order to keep the state consistent, this 381 // method is synchronized 382 renewEntityCapsVersion(); 383 } 384 385 /** 386 * Removes the specified feature from the supported features by this XMPP entity.<p> 387 * 388 * Since no stanza(/packet) is actually sent to the server it is safe to perform this operation 389 * before logging to the server. 390 * 391 * @param feature the feature to remove from the supported features. 392 */ 393 public synchronized void removeFeature(String feature) { 394 features.remove(feature); 395 // Notify others of a state change of SDM. In order to keep the state consistent, this 396 // method is synchronized 397 renewEntityCapsVersion(); 398 } 399 400 /** 401 * Returns true if the specified feature is registered in the ServiceDiscoveryManager. 402 * 403 * @param feature the feature to look for. 404 * @return a boolean indicating if the specified featured is registered or not. 405 */ 406 public synchronized boolean includesFeature(String feature) { 407 return features.contains(feature); 408 } 409 410 /** 411 * Registers extended discovery information of this XMPP entity. When this 412 * client is queried for its information this data form will be returned as 413 * specified by XEP-0128. 414 * <p> 415 * 416 * Since no stanza(/packet) is actually sent to the server it is safe to perform this 417 * operation before logging to the server. In fact, you may want to 418 * configure the extended info before logging to the server so that the 419 * information is already available if it is required upon login. 420 * 421 * @param info 422 * the data form that contains the extend service discovery 423 * information. 424 */ 425 public synchronized void setExtendedInfo(DataForm info) { 426 extendedInfo = info; 427 // Notify others of a state change of SDM. In order to keep the state consistent, this 428 // method is synchronized 429 renewEntityCapsVersion(); 430 } 431 432 /** 433 * Returns the data form that is set as extended information for this Service Discovery instance (XEP-0128) 434 * 435 * @see <a href="http://xmpp.org/extensions/xep-0128.html">XEP-128: Service Discovery Extensions</a> 436 * @return the data form 437 */ 438 public DataForm getExtendedInfo() { 439 return extendedInfo; 440 } 441 442 /** 443 * Returns the data form as List of PacketExtensions, or null if no data form is set. 444 * This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider) 445 * 446 * @return the data form as List of PacketExtensions 447 */ 448 public List<ExtensionElement> getExtendedInfoAsList() { 449 List<ExtensionElement> res = null; 450 if (extendedInfo != null) { 451 res = new ArrayList<ExtensionElement>(1); 452 res.add(extendedInfo); 453 } 454 return res; 455 } 456 457 /** 458 * Removes the data form containing extended service discovery information 459 * from the information returned by this XMPP entity.<p> 460 * 461 * Since no stanza(/packet) is actually sent to the server it is safe to perform this 462 * operation before logging to the server. 463 */ 464 public synchronized void removeExtendedInfo() { 465 extendedInfo = null; 466 // Notify others of a state change of SDM. In order to keep the state consistent, this 467 // method is synchronized 468 renewEntityCapsVersion(); 469 } 470 471 /** 472 * Returns the discovered information of a given XMPP entity addressed by its JID. 473 * Use null as entityID to query the server 474 * 475 * @param entityID the address of the XMPP entity or null. 476 * @return the discovered information. 477 * @throws XMPPErrorException 478 * @throws NoResponseException 479 * @throws NotConnectedException 480 */ 481 public DiscoverInfo discoverInfo(String entityID) throws NoResponseException, XMPPErrorException, NotConnectedException { 482 if (entityID == null) 483 return discoverInfo(null, null); 484 485 // Check if the have it cached in the Entity Capabilities Manager 486 DiscoverInfo info = EntityCapsManager.getDiscoverInfoByUser(entityID); 487 488 if (info != null) { 489 // We were able to retrieve the information from Entity Caps and 490 // avoided a disco request, hurray! 491 return info; 492 } 493 494 // Try to get the newest node#version if it's known, otherwise null is 495 // returned 496 EntityCapsManager.NodeVerHash nvh = EntityCapsManager.getNodeVerHashByJid(entityID); 497 498 // Discover by requesting the information from the remote entity 499 // Note that wee need to use NodeVer as argument for Node if it exists 500 info = discoverInfo(entityID, nvh != null ? nvh.getNodeVer() : null); 501 502 // If the node version is known, store the new entry. 503 if (nvh != null) { 504 if (EntityCapsManager.verifyDiscoverInfoVersion(nvh.getVer(), nvh.getHash(), info)) 505 EntityCapsManager.addDiscoverInfoByNode(nvh.getNodeVer(), info); 506 } 507 508 return info; 509 } 510 511 /** 512 * Returns the discovered information of a given XMPP entity addressed by its JID and 513 * note attribute. Use this message only when trying to query information which is not 514 * directly addressable. 515 * 516 * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a> 517 * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a> 518 * 519 * @param entityID the address of the XMPP entity. 520 * @param node the optional attribute that supplements the 'jid' attribute. 521 * @return the discovered information. 522 * @throws XMPPErrorException if the operation failed for some reason. 523 * @throws NoResponseException if there was no response from the server. 524 * @throws NotConnectedException 525 */ 526 public DiscoverInfo discoverInfo(String entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException { 527 // Discover the entity's info 528 DiscoverInfo disco = new DiscoverInfo(); 529 disco.setType(IQ.Type.get); 530 disco.setTo(entityID); 531 disco.setNode(node); 532 533 Stanza result = connection().createPacketCollectorAndSend(disco).nextResultOrThrow(); 534 535 return (DiscoverInfo) result; 536 } 537 538 /** 539 * Returns the discovered items of a given XMPP entity addressed by its JID. 540 * 541 * @param entityID the address of the XMPP entity. 542 * @return the discovered information. 543 * @throws XMPPErrorException if the operation failed for some reason. 544 * @throws NoResponseException if there was no response from the server. 545 * @throws NotConnectedException 546 */ 547 public DiscoverItems discoverItems(String entityID) throws NoResponseException, XMPPErrorException, NotConnectedException { 548 return discoverItems(entityID, null); 549 } 550 551 /** 552 * Returns the discovered items of a given XMPP entity addressed by its JID and 553 * note attribute. Use this message only when trying to query information which is not 554 * directly addressable. 555 * 556 * @param entityID the address of the XMPP entity. 557 * @param node the optional attribute that supplements the 'jid' attribute. 558 * @return the discovered items. 559 * @throws XMPPErrorException if the operation failed for some reason. 560 * @throws NoResponseException if there was no response from the server. 561 * @throws NotConnectedException 562 */ 563 public DiscoverItems discoverItems(String entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException { 564 // Discover the entity's items 565 DiscoverItems disco = new DiscoverItems(); 566 disco.setType(IQ.Type.get); 567 disco.setTo(entityID); 568 disco.setNode(node); 569 570 Stanza result = connection().createPacketCollectorAndSend(disco).nextResultOrThrow(); 571 return (DiscoverItems) result; 572 } 573 574 /** 575 * Returns true if the server supports publishing of items. A client may wish to publish items 576 * to the server so that the server can provide items associated to the client. These items will 577 * be returned by the server whenever the server receives a disco request targeted to the bare 578 * address of the client (i.e. user@host.com). 579 * 580 * @param entityID the address of the XMPP entity. 581 * @return true if the server supports publishing of items. 582 * @throws XMPPErrorException 583 * @throws NoResponseException 584 * @throws NotConnectedException 585 */ 586 public boolean canPublishItems(String entityID) throws NoResponseException, XMPPErrorException, NotConnectedException { 587 DiscoverInfo info = discoverInfo(entityID); 588 return canPublishItems(info); 589 } 590 591 /** 592 * Returns true if the server supports publishing of items. A client may wish to publish items 593 * to the server so that the server can provide items associated to the client. These items will 594 * be returned by the server whenever the server receives a disco request targeted to the bare 595 * address of the client (i.e. user@host.com). 596 * 597 * @param info the discover info stanza(/packet) to check. 598 * @return true if the server supports publishing of items. 599 */ 600 public static boolean canPublishItems(DiscoverInfo info) { 601 return info.containsFeature("http://jabber.org/protocol/disco#publish"); 602 } 603 604 /** 605 * Publishes new items to a parent entity. The item elements to publish MUST have at least 606 * a 'jid' attribute specifying the Entity ID of the item, and an action attribute which 607 * specifies the action being taken for that item. Possible action values are: "update" and 608 * "remove". 609 * 610 * @param entityID the address of the XMPP entity. 611 * @param discoverItems the DiscoveryItems to publish. 612 * @throws XMPPErrorException 613 * @throws NoResponseException 614 * @throws NotConnectedException 615 */ 616 public void publishItems(String entityID, DiscoverItems discoverItems) throws NoResponseException, XMPPErrorException, NotConnectedException { 617 publishItems(entityID, null, discoverItems); 618 } 619 620 /** 621 * Publishes new items to a parent entity and node. The item elements to publish MUST have at 622 * least a 'jid' attribute specifying the Entity ID of the item, and an action attribute which 623 * specifies the action being taken for that item. Possible action values are: "update" and 624 * "remove". 625 * 626 * @param entityID the address of the XMPP entity. 627 * @param node the attribute that supplements the 'jid' attribute. 628 * @param discoverItems the DiscoveryItems to publish. 629 * @throws XMPPErrorException if the operation failed for some reason. 630 * @throws NoResponseException if there was no response from the server. 631 * @throws NotConnectedException 632 */ 633 public void publishItems(String entityID, String node, DiscoverItems discoverItems) throws NoResponseException, XMPPErrorException, NotConnectedException 634 { 635 discoverItems.setType(IQ.Type.set); 636 discoverItems.setTo(entityID); 637 discoverItems.setNode(node); 638 639 connection().createPacketCollectorAndSend(discoverItems).nextResultOrThrow(); 640 } 641 642 /** 643 * Returns true if the server supports the given feature. 644 * 645 * @param feature 646 * @return true if the server supports the given feature. 647 * @throws NoResponseException 648 * @throws XMPPErrorException 649 * @throws NotConnectedException 650 * @since 4.1 651 */ 652 public boolean serverSupportsFeature(String feature) throws NoResponseException, XMPPErrorException, 653 NotConnectedException { 654 return supportsFeature(connection().getServiceName(), feature); 655 } 656 657 /** 658 * Queries the remote entity for it's features and returns true if the given feature is found. 659 * 660 * @param jid the JID of the remote entity 661 * @param feature 662 * @return true if the entity supports the feature, false otherwise 663 * @throws XMPPErrorException 664 * @throws NoResponseException 665 * @throws NotConnectedException 666 */ 667 public boolean supportsFeature(String jid, String feature) throws NoResponseException, XMPPErrorException, NotConnectedException { 668 DiscoverInfo result = discoverInfo(jid); 669 return result.containsFeature(feature); 670 } 671 672 /** 673 * Create a cache to hold the 25 most recently lookup services for a given feature for a period 674 * of 24 hours. 675 */ 676 private Cache<String, List<String>> services = new ExpirationCache<String, List<String>>(25, 677 24 * 60 * 60 * 1000); 678 679 /** 680 * Find all services under the users service that provide a given feature. 681 * 682 * @param feature the feature to search for 683 * @param stopOnFirst if true, stop searching after the first service was found 684 * @param useCache if true, query a cache first to avoid network I/O 685 * @return a possible empty list of services providing the given feature 686 * @throws NoResponseException 687 * @throws XMPPErrorException 688 * @throws NotConnectedException 689 */ 690 public List<String> findServices(String feature, boolean stopOnFirst, boolean useCache) 691 throws NoResponseException, XMPPErrorException, NotConnectedException { 692 List<String> serviceAddresses = null; 693 String serviceName = connection().getServiceName(); 694 if (useCache) { 695 serviceAddresses = (List<String>) services.get(feature); 696 if (serviceAddresses != null) { 697 return serviceAddresses; 698 } 699 } 700 serviceAddresses = new LinkedList<String>(); 701 // Send the disco packet to the server itself 702 DiscoverInfo info; 703 try { 704 info = discoverInfo(serviceName); 705 } catch (XMPPErrorException e) { 706 // Be extra robust here: Return the empty linked list and log this situation 707 LOGGER.log(Level.WARNING, "Could not discover information about service", e); 708 return serviceAddresses; 709 } 710 // Check if the server supports XEP-33 711 if (info.containsFeature(feature)) { 712 serviceAddresses.add(serviceName); 713 if (stopOnFirst) { 714 if (useCache) { 715 // Cache the discovered information 716 services.put(feature, serviceAddresses); 717 } 718 return serviceAddresses; 719 } 720 } 721 DiscoverItems items; 722 try { 723 // Get the disco items and send the disco packet to each server item 724 items = discoverItems(serviceName); 725 } catch(XMPPErrorException e) { 726 LOGGER.log(Level.WARNING, "Could not discover items about service", e); 727 return serviceAddresses; 728 } 729 for (DiscoverItems.Item item : items.getItems()) { 730 try { 731 // TODO is it OK here in all cases to query without the node attribute? 732 // MultipleRecipientManager queried initially also with the node attribute, but this 733 // could be simply a fault instead of intentional. 734 info = discoverInfo(item.getEntityID()); 735 } 736 catch (XMPPErrorException | NoResponseException e) { 737 // Don't throw this exceptions if one of the server's items fail 738 LOGGER.log(Level.WARNING, "Exception while discovering info for feature " + feature 739 + " of " + item.getEntityID() + " node: " + item.getNode(), e); 740 continue; 741 } 742 if (info.containsFeature(feature)) { 743 serviceAddresses.add(item.getEntityID()); 744 if (stopOnFirst) { 745 break; 746 } 747 } 748 } 749 if (useCache) { 750 // Cache the discovered information 751 services.put(feature, serviceAddresses); 752 } 753 return serviceAddresses; 754 } 755 756 /** 757 * Entity Capabilities 758 */ 759 760 /** 761 * Loads the ServiceDiscoveryManager with an EntityCapsManger that speeds up certain lookups. 762 * 763 * @param manager 764 */ 765 public void setEntityCapsManager(EntityCapsManager manager) { 766 capsManager = manager; 767 } 768 769 /** 770 * Updates the Entity Capabilities Verification String if EntityCaps is enabled. 771 */ 772 private void renewEntityCapsVersion() { 773 if (capsManager != null && capsManager.entityCapsEnabled()) 774 capsManager.updateLocalEntityCaps(); 775 } 776}