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