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; 030import java.util.concurrent.CopyOnWriteArraySet; 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.StanzaError; 045import org.jivesoftware.smack.util.Objects; 046import org.jivesoftware.smack.util.StringUtils; 047 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 final List<DiscoInfoLookupShortcutMechanism> discoInfoLookupShortcutMechanisms = new ArrayList<>(2); 078 079 private static DiscoverInfo.Identity defaultIdentity = new Identity(DEFAULT_IDENTITY_CATEGORY, 080 DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE); 081 082 private final Set<DiscoverInfo.Identity> identities = new HashSet<>(); 083 private DiscoverInfo.Identity identity = defaultIdentity; 084 085 private final Set<EntityCapabilitiesChangedListener> entityCapabilitiesChangedListeners = new CopyOnWriteArraySet<>(); 086 087 private static final Map<XMPPConnection, ServiceDiscoveryManager> instances = new WeakHashMap<>(); 088 089 private final Set<String> features = new HashSet<>(); 090 private DataForm extendedInfo = null; 091 private final Map<String, NodeInformationProvider> nodeInformationProviders = new ConcurrentHashMap<>(); 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(StanzaError.getBuilder(StanzaError.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(StanzaError.getBuilder(StanzaError.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<>(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<>(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 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 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 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<>(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 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 synchronized (discoInfoLookupShortcutMechanisms) { 494 for (DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism : discoInfoLookupShortcutMechanisms) { 495 DiscoverInfo info = discoInfoLookupShortcutMechanism.getDiscoverInfoByUser(this, entityID); 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 } 503 504 // Last resort: Standard discovery. 505 return discoverInfo(entityID, null); 506 } 507 508 /** 509 * Returns the discovered information of a given XMPP entity addressed by its JID and 510 * note attribute. Use this message only when trying to query information which is not 511 * directly addressable. 512 * 513 * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a> 514 * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a> 515 * 516 * @param entityID the address of the XMPP entity. 517 * @param node the optional attribute that supplements the 'jid' attribute. 518 * @return the discovered information. 519 * @throws XMPPErrorException if the operation failed for some reason. 520 * @throws NoResponseException if there was no response from the server. 521 * @throws NotConnectedException 522 * @throws InterruptedException 523 */ 524 public DiscoverInfo discoverInfo(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 525 // Discover the entity's info 526 DiscoverInfo disco = new DiscoverInfo(); 527 disco.setType(IQ.Type.get); 528 disco.setTo(entityID); 529 disco.setNode(node); 530 531 Stanza result = connection().createStanzaCollectorAndSend(disco).nextResultOrThrow(); 532 533 return (DiscoverInfo) result; 534 } 535 536 /** 537 * Returns the discovered items of a given XMPP entity addressed by its JID. 538 * 539 * @param entityID the address of the XMPP entity. 540 * @return the discovered information. 541 * @throws XMPPErrorException if the operation failed for some reason. 542 * @throws NoResponseException if there was no response from the server. 543 * @throws NotConnectedException 544 * @throws InterruptedException 545 */ 546 public DiscoverItems discoverItems(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 547 return discoverItems(entityID, null); 548 } 549 550 /** 551 * Returns the discovered items of a given XMPP entity addressed by its JID and 552 * note attribute. Use this message only when trying to query information which is not 553 * directly addressable. 554 * 555 * @param entityID the address of the XMPP entity. 556 * @param node the optional attribute that supplements the 'jid' attribute. 557 * @return the discovered items. 558 * @throws XMPPErrorException if the operation failed for some reason. 559 * @throws NoResponseException if there was no response from the server. 560 * @throws NotConnectedException 561 * @throws InterruptedException 562 */ 563 public DiscoverItems discoverItems(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 564 // Discover the entity's items 565 DiscoverItems disco = new DiscoverItems(); 566 disco.setType(IQ.Type.get); 567 disco.setTo(entityID); 568 disco.setNode(node); 569 570 Stanza result = connection().createStanzaCollectorAndSend(disco).nextResultOrThrow(); 571 return (DiscoverItems) result; 572 } 573 574 /** 575 * Returns true if the server supports publishing of items. A client may wish to publish items 576 * to the server so that the server can provide items associated to the client. These items will 577 * be returned by the server whenever the server receives a disco request targeted to the bare 578 * address of the client (i.e. user@host.com). 579 * 580 * @param entityID the address of the XMPP entity. 581 * @return true if the server supports publishing of items. 582 * @throws XMPPErrorException 583 * @throws NoResponseException 584 * @throws NotConnectedException 585 * @throws InterruptedException 586 * @deprecated The disco-publish feature was removed from XEP-0030 in 2008 in favor of XEP-0060: Publish-Subscribe. 587 */ 588 @Deprecated 589 // TODO: Remove in Smack 4.4 590 public boolean canPublishItems(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 591 DiscoverInfo info = discoverInfo(entityID); 592 return canPublishItems(info); 593 } 594 595 /** 596 * Returns true if the server supports publishing of items. A client may wish to publish items 597 * to the server so that the server can provide items associated to the client. These items will 598 * be returned by the server whenever the server receives a disco request targeted to the bare 599 * address of the client (i.e. user@host.com). 600 * 601 * @param info the discover info stanza to check. 602 * @return true if the server supports publishing of items. 603 * @deprecated The disco-publish feature was removed from XEP-0030 in 2008 in favor of XEP-0060: Publish-Subscribe. 604 */ 605 @Deprecated 606 // TODO: Remove in Smack 4.4 607 public static boolean canPublishItems(DiscoverInfo info) { 608 return info.containsFeature("http://jabber.org/protocol/disco#publish"); 609 } 610 611 /** 612 * Publishes new items to a parent entity. The item elements to publish MUST have at least 613 * a 'jid' attribute specifying the Entity ID of the item, and an action attribute which 614 * specifies the action being taken for that item. Possible action values are: "update" and 615 * "remove". 616 * 617 * @param entityID the address of the XMPP entity. 618 * @param discoverItems the DiscoveryItems to publish. 619 * @throws XMPPErrorException 620 * @throws NoResponseException 621 * @throws NotConnectedException 622 * @throws InterruptedException 623 * @deprecated The disco-publish feature was removed from XEP-0030 in 2008 in favor of XEP-0060: Publish-Subscribe. 624 */ 625 @Deprecated 626 // TODO: Remove in Smack 4.4 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 * @deprecated The disco-publish feature was removed from XEP-0030 in 2008 in favor of XEP-0060: Publish-Subscribe. 645 */ 646 @Deprecated 647 // TODO: Remove in Smack 4.4 648 public void publishItems(Jid entityID, String node, DiscoverItems discoverItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 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 final 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 DomainBareJid serviceName = connection().getXMPPServiceDomain(); 788 return findServicesDiscoverInfo(serviceName, feature, stopOnFirst, useCache, encounteredExceptions); 789 } 790 791 /** 792 * Find all services under a given service that provide a given feature. 793 * 794 * @param serviceName the service to query 795 * @param feature the feature to search for 796 * @param stopOnFirst if true, stop searching after the first service was found 797 * @param useCache if true, query a cache first to avoid network I/O 798 * @param encounteredExceptions an optional map which will be filled with the exceptions encountered 799 * @return a possible empty list of services providing the given feature 800 * @throws NoResponseException 801 * @throws XMPPErrorException 802 * @throws NotConnectedException 803 * @throws InterruptedException 804 * @since 4.3.0 805 */ 806 public List<DiscoverInfo> findServicesDiscoverInfo(DomainBareJid serviceName, String feature, boolean stopOnFirst, 807 boolean useCache, Map<? super Jid, Exception> encounteredExceptions) 808 throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 809 List<DiscoverInfo> serviceDiscoInfo; 810 if (useCache) { 811 serviceDiscoInfo = services.lookup(feature); 812 if (serviceDiscoInfo != null) { 813 return serviceDiscoInfo; 814 } 815 } 816 serviceDiscoInfo = new LinkedList<>(); 817 // Send the disco packet to the server itself 818 DiscoverInfo info; 819 try { 820 info = discoverInfo(serviceName); 821 } catch (XMPPErrorException e) { 822 if (encounteredExceptions != null) { 823 encounteredExceptions.put(serviceName, e); 824 } 825 return serviceDiscoInfo; 826 } 827 // Check if the server supports the feature 828 if (info.containsFeature(feature)) { 829 serviceDiscoInfo.add(info); 830 if (stopOnFirst) { 831 if (useCache) { 832 // Cache the discovered information 833 services.put(feature, serviceDiscoInfo); 834 } 835 return serviceDiscoInfo; 836 } 837 } 838 DiscoverItems items; 839 try { 840 // Get the disco items and send the disco packet to each server item 841 items = discoverItems(serviceName); 842 } catch (XMPPErrorException e) { 843 if (encounteredExceptions != null) { 844 encounteredExceptions.put(serviceName, e); 845 } 846 return serviceDiscoInfo; 847 } 848 for (DiscoverItems.Item item : items.getItems()) { 849 Jid address = item.getEntityID(); 850 try { 851 // TODO is it OK here in all cases to query without the node attribute? 852 // MultipleRecipientManager queried initially also with the node attribute, but this 853 // could be simply a fault instead of intentional. 854 info = discoverInfo(address); 855 } 856 catch (XMPPErrorException | NoResponseException e) { 857 if (encounteredExceptions != null) { 858 encounteredExceptions.put(address, e); 859 } 860 continue; 861 } 862 if (info.containsFeature(feature)) { 863 serviceDiscoInfo.add(info); 864 if (stopOnFirst) { 865 break; 866 } 867 } 868 } 869 if (useCache) { 870 // Cache the discovered information 871 services.put(feature, serviceDiscoInfo); 872 } 873 return serviceDiscoInfo; 874 } 875 876 /** 877 * Find all services under the users service that provide a given feature. 878 * 879 * @param feature the feature to search for 880 * @param stopOnFirst if true, stop searching after the first service was found 881 * @param useCache if true, query a cache first to avoid network I/O 882 * @return a possible empty list of services providing the given feature 883 * @throws NoResponseException 884 * @throws XMPPErrorException 885 * @throws NotConnectedException 886 * @throws InterruptedException 887 */ 888 public List<DomainBareJid> findServices(String feature, boolean stopOnFirst, boolean useCache) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 889 List<DiscoverInfo> services = findServicesDiscoverInfo(feature, stopOnFirst, useCache); 890 List<DomainBareJid> res = new ArrayList<>(services.size()); 891 for (DiscoverInfo info : services) { 892 res.add(info.getFrom().asDomainBareJid()); 893 } 894 return res; 895 } 896 897 public DomainBareJid findService(String feature, boolean useCache, String category, String type) 898 throws NoResponseException, XMPPErrorException, NotConnectedException, 899 InterruptedException { 900 boolean noCategory = StringUtils.isNullOrEmpty(category); 901 boolean noType = StringUtils.isNullOrEmpty(type); 902 if (noType != noCategory) { 903 throw new IllegalArgumentException("Must specify either both, category and type, or none"); 904 } 905 906 List<DiscoverInfo> services = findServicesDiscoverInfo(feature, false, useCache); 907 if (services.isEmpty()) { 908 return null; 909 } 910 911 if (!noCategory && !noType) { 912 for (DiscoverInfo info : services) { 913 if (info.hasIdentity(category, type)) { 914 return info.getFrom().asDomainBareJid(); 915 } 916 } 917 } 918 919 return services.get(0).getFrom().asDomainBareJid(); 920 } 921 922 public DomainBareJid findService(String feature, boolean useCache) throws NoResponseException, 923 XMPPErrorException, NotConnectedException, InterruptedException { 924 return findService(feature, useCache, null, null); 925 } 926 927 public boolean addEntityCapabilitiesChangedListener(EntityCapabilitiesChangedListener entityCapabilitiesChangedListener) { 928 return entityCapabilitiesChangedListeners.add(entityCapabilitiesChangedListener); 929 } 930 931 /** 932 * Notify the {@link EntityCapabilitiesChangedListener} about changed capabilities. 933 */ 934 private void renewEntityCapsVersion() { 935 for (EntityCapabilitiesChangedListener entityCapabilitiesChangedListener : entityCapabilitiesChangedListeners) { 936 entityCapabilitiesChangedListener.onEntityCapailitiesChanged(); 937 } 938 } 939 940 public static void addDiscoInfoLookupShortcutMechanism(DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism) { 941 synchronized (discoInfoLookupShortcutMechanisms) { 942 discoInfoLookupShortcutMechanisms.add(discoInfoLookupShortcutMechanism); 943 Collections.sort(discoInfoLookupShortcutMechanisms); 944 } 945 } 946 947 public static void removeDiscoInfoLookupShortcutMechanism(DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism) { 948 synchronized (discoInfoLookupShortcutMechanisms) { 949 discoInfoLookupShortcutMechanisms.remove(discoInfoLookupShortcutMechanism); 950 } 951 } 952}