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