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