001/** 002 * 003 * Copyright 2003-2007 Jive Software, 2018 Florian Schmaus. 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; 030 031import org.jivesoftware.smack.ConnectionCreationListener; 032import org.jivesoftware.smack.Manager; 033import org.jivesoftware.smack.SmackException.NoResponseException; 034import org.jivesoftware.smack.SmackException.NotConnectedException; 035import org.jivesoftware.smack.XMPPConnection; 036import org.jivesoftware.smack.XMPPConnectionRegistry; 037import org.jivesoftware.smack.XMPPException.XMPPErrorException; 038import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; 039import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; 040import org.jivesoftware.smack.packet.ExtensionElement; 041import org.jivesoftware.smack.packet.IQ; 042import org.jivesoftware.smack.packet.Stanza; 043import org.jivesoftware.smack.packet.XMPPError; 044import org.jivesoftware.smack.util.Objects; 045import org.jivesoftware.smack.util.StringUtils; 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 * @author Florian Schmaus 070 */ 071public final class ServiceDiscoveryManager extends Manager { 072 073 private static final String DEFAULT_IDENTITY_NAME = "Smack"; 074 private static final String DEFAULT_IDENTITY_CATEGORY = "client"; 075 private static final String DEFAULT_IDENTITY_TYPE = "pc"; 076 077 private static DiscoverInfo.Identity defaultIdentity = new Identity(DEFAULT_IDENTITY_CATEGORY, 078 DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE); 079 080 private final Set<DiscoverInfo.Identity> identities = new HashSet<>(); 081 private DiscoverInfo.Identity identity = defaultIdentity; 082 083 private EntityCapsManager capsManager; 084 085 private static final Map<XMPPConnection, ServiceDiscoveryManager> instances = new WeakHashMap<>(); 086 087 private final Set<String> features = new HashSet<>(); 088 private DataForm extendedInfo = null; 089 private final Map<String, NodeInformationProvider> nodeInformationProviders = new ConcurrentHashMap<>(); 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<>(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<>(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<>(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 * @deprecated The disco-publish feature was removed from XEP-0030 in 2008 in favor of XEP-0060: Publish-Subscribe. 596 */ 597 @Deprecated 598 // TODO: Remove in Smack 4.4 599 public boolean canPublishItems(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 600 DiscoverInfo info = discoverInfo(entityID); 601 return canPublishItems(info); 602 } 603 604 /** 605 * Returns true if the server supports publishing of items. A client may wish to publish items 606 * to the server so that the server can provide items associated to the client. These items will 607 * be returned by the server whenever the server receives a disco request targeted to the bare 608 * address of the client (i.e. user@host.com). 609 * 610 * @param info the discover info stanza(/packet) to check. 611 * @return true if the server supports publishing of items. 612 * @deprecated The disco-publish feature was removed from XEP-0030 in 2008 in favor of XEP-0060: Publish-Subscribe. 613 */ 614 @Deprecated 615 // TODO: Remove in Smack 4.4 616 public static boolean canPublishItems(DiscoverInfo info) { 617 return info.containsFeature("http://jabber.org/protocol/disco#publish"); 618 } 619 620 /** 621 * Publishes new items to a parent entity. The item elements to publish MUST have at least 622 * a 'jid' attribute specifying the Entity ID of the item, and an action attribute which 623 * specifies the action being taken for that item. Possible action values are: "update" and 624 * "remove". 625 * 626 * @param entityID the address of the XMPP entity. 627 * @param discoverItems the DiscoveryItems to publish. 628 * @throws XMPPErrorException 629 * @throws NoResponseException 630 * @throws NotConnectedException 631 * @throws InterruptedException 632 * @deprecated The disco-publish feature was removed from XEP-0030 in 2008 in favor of XEP-0060: Publish-Subscribe. 633 */ 634 @Deprecated 635 // TODO: Remove in Smack 4.4 636 public void publishItems(Jid entityID, DiscoverItems discoverItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 637 publishItems(entityID, null, discoverItems); 638 } 639 640 /** 641 * Publishes new items to a parent entity and node. The item elements to publish MUST have at 642 * least a 'jid' attribute specifying the Entity ID of the item, and an action attribute which 643 * specifies the action being taken for that item. Possible action values are: "update" and 644 * "remove". 645 * 646 * @param entityID the address of the XMPP entity. 647 * @param node the attribute that supplements the 'jid' attribute. 648 * @param discoverItems the DiscoveryItems to publish. 649 * @throws XMPPErrorException if the operation failed for some reason. 650 * @throws NoResponseException if there was no response from the server. 651 * @throws NotConnectedException 652 * @throws InterruptedException 653 * @deprecated The disco-publish feature was removed from XEP-0030 in 2008 in favor of XEP-0060: Publish-Subscribe. 654 */ 655 @Deprecated 656 // TODO: Remove in Smack 4.4 657 public void publishItems(Jid entityID, String node, DiscoverItems discoverItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException 658 { 659 discoverItems.setType(IQ.Type.set); 660 discoverItems.setTo(entityID); 661 discoverItems.setNode(node); 662 663 connection().createStanzaCollectorAndSend(discoverItems).nextResultOrThrow(); 664 } 665 666 /** 667 * Returns true if the server supports the given feature. 668 * 669 * @param feature 670 * @return true if the server supports the given feature. 671 * @throws NoResponseException 672 * @throws XMPPErrorException 673 * @throws NotConnectedException 674 * @throws InterruptedException 675 * @since 4.1 676 */ 677 public boolean serverSupportsFeature(CharSequence feature) throws NoResponseException, XMPPErrorException, 678 NotConnectedException, InterruptedException { 679 return serverSupportsFeatures(feature); 680 } 681 682 public boolean serverSupportsFeatures(CharSequence... features) throws NoResponseException, 683 XMPPErrorException, NotConnectedException, InterruptedException { 684 return serverSupportsFeatures(Arrays.asList(features)); 685 } 686 687 public boolean serverSupportsFeatures(Collection<? extends CharSequence> features) 688 throws NoResponseException, XMPPErrorException, NotConnectedException, 689 InterruptedException { 690 return supportsFeatures(connection().getXMPPServiceDomain(), features); 691 } 692 693 /** 694 * Check if the given features are supported by the connection account. This means that the discovery information 695 * lookup will be performed on the bare JID of the connection managed by this ServiceDiscoveryManager. 696 * 697 * @param features the features to check 698 * @return <code>true</code> if all features are supported by the connection account, <code>false</code> otherwise 699 * @throws NoResponseException 700 * @throws XMPPErrorException 701 * @throws NotConnectedException 702 * @throws InterruptedException 703 * @since 4.2.2 704 */ 705 public boolean accountSupportsFeatures(CharSequence... features) 706 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 707 return accountSupportsFeatures(Arrays.asList(features)); 708 } 709 710 /** 711 * Check if the given collection of features are supported by the connection account. This means that the discovery 712 * information lookup will be performed on the bare JID of the connection managed by this ServiceDiscoveryManager. 713 * 714 * @param features a collection of features 715 * @return <code>true</code> if all features are supported by the connection account, <code>false</code> otherwise 716 * @throws NoResponseException 717 * @throws XMPPErrorException 718 * @throws NotConnectedException 719 * @throws InterruptedException 720 * @since 4.2.2 721 */ 722 public boolean accountSupportsFeatures(Collection<? extends CharSequence> features) 723 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 724 EntityBareJid accountJid = connection().getUser().asEntityBareJid(); 725 return supportsFeatures(accountJid, features); 726 } 727 728 /** 729 * Queries the remote entity for it's features and returns true if the given feature is found. 730 * 731 * @param jid the JID of the remote entity 732 * @param feature 733 * @return true if the entity supports the feature, false otherwise 734 * @throws XMPPErrorException 735 * @throws NoResponseException 736 * @throws NotConnectedException 737 * @throws InterruptedException 738 */ 739 public boolean supportsFeature(Jid jid, CharSequence feature) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 740 return supportsFeatures(jid, feature); 741 } 742 743 public boolean supportsFeatures(Jid jid, CharSequence... features) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 744 return supportsFeatures(jid, Arrays.asList(features)); 745 } 746 747 public boolean supportsFeatures(Jid jid, Collection<? extends CharSequence> features) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 748 DiscoverInfo result = discoverInfo(jid); 749 for (CharSequence feature : features) { 750 if (!result.containsFeature(feature)) { 751 return false; 752 } 753 } 754 return true; 755 } 756 757 /** 758 * Create a cache to hold the 25 most recently lookup services for a given feature for a period 759 * of 24 hours. 760 */ 761 private final Cache<String, List<DiscoverInfo>> services = new ExpirationCache<>(25, 762 24 * 60 * 60 * 1000); 763 764 /** 765 * Find all services under the users service that provide a given feature. 766 * 767 * @param feature the feature to search for 768 * @param stopOnFirst if true, stop searching after the first service was found 769 * @param useCache if true, query a cache first to avoid network I/O 770 * @return a possible empty list of services providing the given feature 771 * @throws NoResponseException 772 * @throws XMPPErrorException 773 * @throws NotConnectedException 774 * @throws InterruptedException 775 */ 776 public List<DiscoverInfo> findServicesDiscoverInfo(String feature, boolean stopOnFirst, boolean useCache) 777 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 778 return findServicesDiscoverInfo(feature, stopOnFirst, useCache, null); 779 } 780 781 /** 782 * Find all services under the users service that provide a given feature. 783 * 784 * @param feature the feature to search for 785 * @param stopOnFirst if true, stop searching after the first service was found 786 * @param useCache if true, query a cache first to avoid network I/O 787 * @param encounteredExceptions an optional map which will be filled with the exceptions encountered 788 * @return a possible empty list of services providing the given feature 789 * @throws NoResponseException 790 * @throws XMPPErrorException 791 * @throws NotConnectedException 792 * @throws InterruptedException 793 * @since 4.2.2 794 */ 795 public List<DiscoverInfo> findServicesDiscoverInfo(String feature, boolean stopOnFirst, boolean useCache, Map<? super Jid, Exception> encounteredExceptions) 796 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 797 List<DiscoverInfo> serviceDiscoInfo; 798 DomainBareJid serviceName = connection().getXMPPServiceDomain(); 799 if (useCache) { 800 serviceDiscoInfo = services.lookup(feature); 801 if (serviceDiscoInfo != null) { 802 return serviceDiscoInfo; 803 } 804 } 805 serviceDiscoInfo = new LinkedList<>(); 806 // Send the disco packet to the server itself 807 DiscoverInfo info; 808 try { 809 info = discoverInfo(serviceName); 810 } catch (XMPPErrorException e) { 811 if (encounteredExceptions != null) { 812 encounteredExceptions.put(serviceName, e); 813 } 814 return serviceDiscoInfo; 815 } 816 // Check if the server supports the feature 817 if (info.containsFeature(feature)) { 818 serviceDiscoInfo.add(info); 819 if (stopOnFirst) { 820 if (useCache) { 821 // Cache the discovered information 822 services.put(feature, serviceDiscoInfo); 823 } 824 return serviceDiscoInfo; 825 } 826 } 827 DiscoverItems items; 828 try { 829 // Get the disco items and send the disco packet to each server item 830 items = discoverItems(serviceName); 831 } catch (XMPPErrorException e) { 832 if (encounteredExceptions != null) { 833 encounteredExceptions.put(serviceName, e); 834 } 835 return serviceDiscoInfo; 836 } 837 for (DiscoverItems.Item item : items.getItems()) { 838 Jid address = item.getEntityID(); 839 try { 840 // TODO is it OK here in all cases to query without the node attribute? 841 // MultipleRecipientManager queried initially also with the node attribute, but this 842 // could be simply a fault instead of intentional. 843 info = discoverInfo(address); 844 } 845 catch (XMPPErrorException | NoResponseException e) { 846 if (encounteredExceptions != null) { 847 encounteredExceptions.put(address, e); 848 } 849 continue; 850 } 851 if (info.containsFeature(feature)) { 852 serviceDiscoInfo.add(info); 853 if (stopOnFirst) { 854 break; 855 } 856 } 857 } 858 if (useCache) { 859 // Cache the discovered information 860 services.put(feature, serviceDiscoInfo); 861 } 862 return serviceDiscoInfo; 863 } 864 865 /** 866 * Find all services under the users service that provide a given feature. 867 * 868 * @param feature the feature to search for 869 * @param stopOnFirst if true, stop searching after the first service was found 870 * @param useCache if true, query a cache first to avoid network I/O 871 * @return a possible empty list of services providing the given feature 872 * @throws NoResponseException 873 * @throws XMPPErrorException 874 * @throws NotConnectedException 875 * @throws InterruptedException 876 */ 877 public List<DomainBareJid> findServices(String feature, boolean stopOnFirst, boolean useCache) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 878 List<DiscoverInfo> services = findServicesDiscoverInfo(feature, stopOnFirst, useCache); 879 List<DomainBareJid> res = new ArrayList<>(services.size()); 880 for (DiscoverInfo info : services) { 881 res.add(info.getFrom().asDomainBareJid()); 882 } 883 return res; 884 } 885 886 public DomainBareJid findService(String feature, boolean useCache, String category, String type) 887 throws NoResponseException, XMPPErrorException, NotConnectedException, 888 InterruptedException { 889 boolean noCategory = StringUtils.isNullOrEmpty(category); 890 boolean noType = StringUtils.isNullOrEmpty(type); 891 if (noType != noCategory) { 892 throw new IllegalArgumentException("Must specify either both, category and type, or none"); 893 } 894 895 List<DiscoverInfo> services = findServicesDiscoverInfo(feature, false, useCache); 896 if (services.isEmpty()) { 897 return null; 898 } 899 900 if (!noCategory && !noType) { 901 for (DiscoverInfo info : services) { 902 if (info.hasIdentity(category, type)) { 903 return info.getFrom().asDomainBareJid(); 904 } 905 } 906 } 907 908 return services.get(0).getFrom().asDomainBareJid(); 909 } 910 911 public DomainBareJid findService(String feature, boolean useCache) throws NoResponseException, 912 XMPPErrorException, NotConnectedException, InterruptedException { 913 return findService(feature, useCache, null, null); 914 } 915 916 /** 917 * Entity Capabilities 918 */ 919 920 /** 921 * Loads the ServiceDiscoveryManager with an EntityCapsManger that speeds up certain lookups. 922 * 923 * @param manager 924 */ 925 public void setEntityCapsManager(EntityCapsManager manager) { 926 capsManager = manager; 927 } 928 929 /** 930 * Updates the Entity Capabilities Verification String if EntityCaps is enabled. 931 */ 932 private void renewEntityCapsVersion() { 933 if (capsManager != null && capsManager.entityCapsEnabled()) 934 capsManager.updateLocalEntityCaps(); 935 } 936}