ServiceDiscoveryManager.java

  1. /**
  2.  *
  3.  * Copyright 2003-2007 Jive Software, 2018-2024 Florian Schmaus.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smackx.disco;

  18. import java.io.IOException;
  19. import java.util.ArrayList;
  20. import java.util.Arrays;
  21. import java.util.Collection;
  22. import java.util.Collections;
  23. import java.util.HashSet;
  24. import java.util.LinkedList;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Set;
  28. import java.util.WeakHashMap;
  29. import java.util.concurrent.ConcurrentHashMap;
  30. import java.util.concurrent.CopyOnWriteArraySet;
  31. import java.util.concurrent.TimeUnit;
  32. import java.util.concurrent.atomic.AtomicInteger;
  33. import java.util.logging.Level;
  34. import java.util.logging.Logger;

  35. import org.jivesoftware.smack.ConnectionCreationListener;
  36. import org.jivesoftware.smack.ConnectionListener;
  37. import org.jivesoftware.smack.Manager;
  38. import org.jivesoftware.smack.ScheduledAction;
  39. import org.jivesoftware.smack.SmackException.NoResponseException;
  40. import org.jivesoftware.smack.SmackException.NotConnectedException;
  41. import org.jivesoftware.smack.XMPPConnection;
  42. import org.jivesoftware.smack.XMPPConnectionRegistry;
  43. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  44. import org.jivesoftware.smack.filter.PresenceTypeFilter;
  45. import org.jivesoftware.smack.internal.AbstractStats;
  46. import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
  47. import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode;
  48. import org.jivesoftware.smack.packet.IQ;
  49. import org.jivesoftware.smack.packet.Presence;
  50. import org.jivesoftware.smack.packet.Stanza;
  51. import org.jivesoftware.smack.packet.StanzaError;
  52. import org.jivesoftware.smack.util.CollectionUtil;
  53. import org.jivesoftware.smack.util.ExtendedAppendable;
  54. import org.jivesoftware.smack.util.Objects;
  55. import org.jivesoftware.smack.util.StringUtils;

  56. import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
  57. import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity;
  58. import org.jivesoftware.smackx.disco.packet.DiscoverInfoBuilder;
  59. import org.jivesoftware.smackx.disco.packet.DiscoverItems;
  60. import org.jivesoftware.smackx.xdata.packet.DataForm;

  61. import org.jxmpp.jid.DomainBareJid;
  62. import org.jxmpp.jid.EntityBareJid;
  63. import org.jxmpp.jid.Jid;
  64. import org.jxmpp.util.cache.Cache;
  65. import org.jxmpp.util.cache.ExpirationCache;

  66. /**
  67.  * Manages discovery of services in XMPP entities. This class provides:
  68.  * <ol>
  69.  * <li>A registry of supported features in this XMPP entity.
  70.  * <li>Automatic response when this XMPP entity is queried for information.
  71.  * <li>Ability to discover items and information of remote XMPP entities.
  72.  * <li>Ability to publish publicly available items.
  73.  * </ol>
  74.  *
  75.  * @author Gaston Dombiak
  76.  * @author Florian Schmaus
  77.  */
  78. public final class ServiceDiscoveryManager extends Manager {

  79.     private static final Logger LOGGER = Logger.getLogger(ServiceDiscoveryManager.class.getName());

  80.     private static final String DEFAULT_IDENTITY_NAME = "Smack";
  81.     private static final String DEFAULT_IDENTITY_CATEGORY = "client";
  82.     private static final String DEFAULT_IDENTITY_TYPE = "pc";

  83.     private static final List<DiscoInfoLookupShortcutMechanism> discoInfoLookupShortcutMechanisms = new ArrayList<>(2);

  84.     private static DiscoverInfo.Identity defaultIdentity = new Identity(DEFAULT_IDENTITY_CATEGORY,
  85.             DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE);

  86.     private final Set<DiscoverInfo.Identity> identities = new HashSet<>();
  87.     private DiscoverInfo.Identity identity = defaultIdentity;

  88.     private final Set<EntityCapabilitiesChangedListener> entityCapabilitiesChangedListeners = new CopyOnWriteArraySet<>();

  89.     private static final Map<XMPPConnection, ServiceDiscoveryManager> instances = new WeakHashMap<>();

  90.     private final Set<String> features = new HashSet<>();
  91.     private List<DataForm> extendedInfos = new ArrayList<>(2);
  92.     private final Map<String, NodeInformationProvider> nodeInformationProviders = new ConcurrentHashMap<>();

  93.     private volatile Presence presenceSend;

  94.     // Create a new ServiceDiscoveryManager on every established connection
  95.     static {
  96.         XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
  97.             @Override
  98.             public void connectionCreated(XMPPConnection connection) {
  99.                 getInstanceFor(connection);
  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 TODO javadoc me please
  108.      */
  109.     public static void setDefaultIdentity(DiscoverInfo.Identity identity) {
  110.         defaultIdentity = identity;
  111.     }

  112.     /**
  113.      * Creates a new ServiceDiscoveryManager for a given XMPPConnection. This means that the
  114.      * service manager will respond to any service discovery request that the connection may
  115.      * receive.
  116.      *
  117.      * @param connection the connection to which a ServiceDiscoveryManager is going to be created.
  118.      */
  119.     private ServiceDiscoveryManager(XMPPConnection connection) {
  120.         super(connection);

  121.         addFeature(DiscoverInfo.NAMESPACE);
  122.         addFeature(DiscoverItems.NAMESPACE);

  123.         // Listen for disco#items requests and answer with an empty result
  124.         connection.registerIQRequestHandler(new AbstractIqRequestHandler(DiscoverItems.ELEMENT, DiscoverItems.NAMESPACE, IQ.Type.get, Mode.async) {
  125.             @Override
  126.             public IQ handleIQRequest(IQ iqRequest) {
  127.                 DiscoverItems discoverItems = (DiscoverItems) iqRequest;
  128.                 DiscoverItems response = new DiscoverItems();
  129.                 response.setType(IQ.Type.result);
  130.                 response.setTo(discoverItems.getFrom());
  131.                 response.setStanzaId(discoverItems.getStanzaId());
  132.                 response.setNode(discoverItems.getNode());

  133.                 // Add the defined items related to the requested node. Look for
  134.                 // the NodeInformationProvider associated with the requested node.
  135.                 NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverItems.getNode());
  136.                 if (nodeInformationProvider != null) {
  137.                     // Specified node was found, add node items
  138.                     response.addItems(nodeInformationProvider.getNodeItems());
  139.                     // Add packet extensions
  140.                     response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
  141.                 } else if (discoverItems.getNode() != null) {
  142.                     // Return <item-not-found/> error since client doesn't contain
  143.                     // the specified node
  144.                     response.setType(IQ.Type.error);
  145.                     response.setError(StanzaError.getBuilder(StanzaError.Condition.item_not_found).build());
  146.                 }
  147.                 return response;
  148.             }
  149.         });

  150.         // Listen for disco#info requests and answer the client's supported features
  151.         // To add a new feature as supported use the #addFeature message
  152.         connection.registerIQRequestHandler(new AbstractIqRequestHandler(DiscoverInfo.ELEMENT, DiscoverInfo.NAMESPACE, IQ.Type.get, Mode.async) {
  153.             @Override
  154.             public IQ handleIQRequest(IQ iqRequest) {
  155.                 DiscoverInfo discoverInfo = (DiscoverInfo) iqRequest;
  156.                 // Answer the client's supported features if the request is of the GET type
  157.                 DiscoverInfoBuilder responseBuilder = DiscoverInfoBuilder.buildResponseFor(discoverInfo, IQ.ResponseType.result);

  158.                 // Add the client's identity and features only if "node" is null
  159.                 // and if the request was not send to a node. If Entity Caps are
  160.                 // enabled the client's identity and features are may also added
  161.                 // if the right node is chosen
  162.                 if (discoverInfo.getNode() == null) {
  163.                     addDiscoverInfoTo(responseBuilder);
  164.                 } else {
  165.                     // Disco#info was sent to a node. Check if we have information of the
  166.                     // specified node
  167.                     NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverInfo.getNode());
  168.                     if (nodeInformationProvider != null) {
  169.                         // Node was found. Add node features
  170.                         responseBuilder.addFeatures(nodeInformationProvider.getNodeFeatures());
  171.                         // Add node identities
  172.                         responseBuilder.addIdentities(nodeInformationProvider.getNodeIdentities());
  173.                         // Add packet extensions
  174.                         responseBuilder.addOptExtensions(nodeInformationProvider.getNodePacketExtensions());
  175.                     } else {
  176.                         // Return <item-not-found/> error since specified node was not found
  177.                         responseBuilder.ofType(IQ.Type.error);
  178.                         responseBuilder.setError(StanzaError.getBuilder(StanzaError.Condition.item_not_found).build());
  179.                     }
  180.                 }

  181.                 DiscoverInfo response = responseBuilder.build();
  182.                 return response;
  183.             }
  184.         });

  185.         connection.addConnectionListener(new ConnectionListener() {
  186.             @Override
  187.             public void authenticated(XMPPConnection connection, boolean resumed) {
  188.                 // Reset presenceSend when the connection was not resumed
  189.                 if (!resumed) {
  190.                     presenceSend = null;
  191.                 }
  192.             }
  193.         });
  194.         connection.addStanzaSendingListener(p -> presenceSend = (Presence) p,
  195.                         PresenceTypeFilter.OUTGOING_PRESENCE_BROADCAST);
  196.     }

  197.     /**
  198.      * Returns the name of the client that will be returned when asked for the client identity
  199.      * in a disco request. The name could be any value you need to identity this client.
  200.      *
  201.      * @return the name of the client that will be returned when asked for the client identity
  202.      *          in a disco request.
  203.      */
  204.     public String getIdentityName() {
  205.         return identity.getName();
  206.     }

  207.     /**
  208.      * Sets the default identity the client will report.
  209.      *
  210.      * @param identity TODO javadoc me please
  211.      */
  212.     public synchronized void setIdentity(Identity identity) {
  213.         this.identity = Objects.requireNonNull(identity, "Identity can not be null");
  214.         // Notify others of a state change of SDM. In order to keep the state consistent, this
  215.         // method is synchronized
  216.         renewEntityCapsVersion();
  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.      * Returns the type of client that will be returned when asked for the client identity in a
  228.      * disco request. The valid types are defined by the category client. Follow this link to learn
  229.      * the possible types: <a href="https://xmpp.org/registrar/disco-categories.html">XMPP Registry for Service Discovery Identities</a>
  230.      *
  231.      * @return the type of client that will be returned when asked for the client identity in a
  232.      *          disco request.
  233.      */
  234.     public String getIdentityType() {
  235.         return identity.getType();
  236.     }

  237.     /**
  238.      * Add an further identity to the client.
  239.      *
  240.      * @param identity TODO javadoc me please
  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.      * Remove an identity from the client. Note that the client needs at least one identity, the default identity, which
  250.      * can not be removed.
  251.      *
  252.      * @param identity TODO javadoc me please
  253.      * @return true, if successful. Otherwise the default identity was given.
  254.      */
  255.     public synchronized boolean removeIdentity(DiscoverInfo.Identity identity) {
  256.         if (identity.equals(this.identity)) return false;
  257.         identities.remove(identity);
  258.         // Notify others of a state change of SDM. In order to keep the state consistent, this
  259.         // method is synchronized
  260.         renewEntityCapsVersion();
  261.         return true;
  262.     }

  263.     /**
  264.      * Returns all identities of this client as unmodifiable Collection.
  265.      *
  266.      * @return all identities as a set
  267.      */
  268.     public Set<DiscoverInfo.Identity> getIdentities() {
  269.         Set<Identity> res = new HashSet<>(identities);
  270.         // Add the main identity that must exist
  271.         res.add(identity);
  272.         return Collections.unmodifiableSet(res);
  273.     }

  274.     /**
  275.      * Returns the ServiceDiscoveryManager instance associated with a given XMPPConnection.
  276.      *
  277.      * @param connection the connection used to look for the proper ServiceDiscoveryManager.
  278.      * @return the ServiceDiscoveryManager associated with a given XMPPConnection.
  279.      */
  280.     public static synchronized ServiceDiscoveryManager getInstanceFor(XMPPConnection connection) {
  281.         ServiceDiscoveryManager sdm = instances.get(connection);
  282.         if (sdm == null) {
  283.             sdm = new ServiceDiscoveryManager(connection);
  284.             // Register the new instance and associate it with the connection
  285.             instances.put(connection, sdm);
  286.         }
  287.         return sdm;
  288.     }

  289.     /**
  290.      * Add discover info response data.
  291.      *
  292.      * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol; Example 2</a>
  293.      *
  294.      * @param response the discover info response packet
  295.      */
  296.     public synchronized void addDiscoverInfoTo(DiscoverInfoBuilder response) {
  297.         // First add the identities of the connection
  298.         response.addIdentities(getIdentities());

  299.         // Add the registered features to the response
  300.         for (String feature : getFeatures()) {
  301.             response.addFeature(feature);
  302.         }

  303.         response.addExtensions(extendedInfos);
  304.     }

  305.     /**
  306.      * Returns the NodeInformationProvider responsible for providing information
  307.      * (ie items) related to a given node or <code>null</null> if none.<p>
  308.      *
  309.      * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
  310.      * NodeInformationProvider will provide information about the rooms where the user has joined.
  311.      *
  312.      * @param node the node that contains items associated with an entity not addressable as a JID.
  313.      * @return the NodeInformationProvider responsible for providing information related
  314.      * to a given node.
  315.      */
  316.     private NodeInformationProvider getNodeInformationProvider(String node) {
  317.         if (node == null) {
  318.             return null;
  319.         }
  320.         return nodeInformationProviders.get(node);
  321.     }

  322.     /**
  323.      * Sets the NodeInformationProvider responsible for providing information
  324.      * (ie items) related to a given node. Every time this client receives a disco request
  325.      * regarding the items of a given node, the provider associated to that node will be the
  326.      * responsible for providing the requested information.<p>
  327.      *
  328.      * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
  329.      * NodeInformationProvider will provide information about the rooms where the user has joined.
  330.      *
  331.      * @param node the node whose items will be provided by the NodeInformationProvider.
  332.      * @param listener the NodeInformationProvider responsible for providing items related
  333.      *      to the node.
  334.      */
  335.     public void setNodeInformationProvider(String node, NodeInformationProvider listener) {
  336.         nodeInformationProviders.put(node, listener);
  337.     }

  338.     /**
  339.      * Removes the NodeInformationProvider responsible for providing information
  340.      * (ie items) related to a given node. This means that no more information will be
  341.      * available for the specified node.
  342.      *
  343.      * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
  344.      * NodeInformationProvider will provide information about the rooms where the user has joined.
  345.      *
  346.      * @param node the node to remove the associated NodeInformationProvider.
  347.      */
  348.     public void removeNodeInformationProvider(String node) {
  349.         nodeInformationProviders.remove(node);
  350.     }

  351.     /**
  352.      * Returns the supported features by this XMPP entity.
  353.      * <p>
  354.      * The result is a copied modifiable list of the original features.
  355.      * </p>
  356.      *
  357.      * @return a List of the supported features by this XMPP entity.
  358.      */
  359.     public synchronized List<String> getFeatures() {
  360.         return new ArrayList<>(features);
  361.     }

  362.     /**
  363.      * Registers that a new feature is supported by this XMPP entity. When this client is
  364.      * queried for its information the registered features will be answered.<p>
  365.      *
  366.      * Since no stanza is actually sent to the server it is safe to perform this operation
  367.      * before logging to the server. In fact, you may want to configure the supported features
  368.      * before logging to the server so that the information is already available if it is required
  369.      * upon login.
  370.      *
  371.      * @param feature the feature to register as supported.
  372.      */
  373.     public synchronized void addFeature(String feature) {
  374.         features.add(feature);
  375.         // Notify others of a state change of SDM. In order to keep the state consistent, this
  376.         // method is synchronized
  377.         renewEntityCapsVersion();
  378.     }

  379.     /**
  380.      * Removes the specified feature from the supported features by this XMPP entity.<p>
  381.      *
  382.      * Since no stanza is actually sent to the server it is safe to perform this operation
  383.      * before logging to the server.
  384.      *
  385.      * @param feature the feature to remove from the supported features.
  386.      */
  387.     public synchronized void removeFeature(String feature) {
  388.         features.remove(feature);
  389.         // Notify others of a state change of SDM. In order to keep the state consistent, this
  390.         // method is synchronized
  391.         renewEntityCapsVersion();
  392.     }

  393.     /**
  394.      * Returns true if the specified feature is registered in the ServiceDiscoveryManager.
  395.      *
  396.      * @param feature the feature to look for.
  397.      * @return a boolean indicating if the specified featured is registered or not.
  398.      */
  399.     public synchronized boolean includesFeature(String feature) {
  400.         return features.contains(feature);
  401.     }

  402.     /**
  403.      * Registers extended discovery information of this XMPP entity. When this
  404.      * client is queried for its information this data form will be returned as
  405.      * specified by XEP-0128.
  406.      * <p>
  407.      *
  408.      * Since no stanza is actually sent to the server it is safe to perform this
  409.      * operation before logging to the server. In fact, you may want to
  410.      * configure the extended info before logging to the server so that the
  411.      * information is already available if it is required upon login.
  412.      *
  413.      * @param extendedInfo the data form that contains the extend service discovery information.
  414.      * @return the old data form which got replaced (if any)
  415.      * @since 4.4.0
  416.      */
  417.     public DataForm addExtendedInfo(DataForm extendedInfo) {
  418.         String formType = extendedInfo.getFormType();
  419.         StringUtils.requireNotNullNorEmpty(formType, "The data form must have a form type set");

  420.         DataForm removedDataForm;
  421.         synchronized (this) {
  422.             removedDataForm = DataForm.remove(extendedInfos, formType);

  423.             extendedInfos.add(extendedInfo);

  424.             // Notify others of a state change of SDM. In order to keep the state consistent, this
  425.             // method is synchronized
  426.             renewEntityCapsVersion();
  427.         }
  428.         return removedDataForm;
  429.     }

  430.     /**
  431.      * Remove the extended discovery information of the given form type.
  432.      *
  433.      * @param formType the type of the data form with the extended discovery information to remove.
  434.      * @since 4.4.0
  435.      */
  436.     public synchronized void removeExtendedInfo(String formType) {
  437.         DataForm removedForm = DataForm.remove(extendedInfos, formType);
  438.         if (removedForm != null) {
  439.             renewEntityCapsVersion();
  440.         }
  441.     }

  442.     /**
  443.      * Returns the data form as List of PacketExtensions, or null if no data form is set.
  444.      * This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider)
  445.      *
  446.      * @return the data form as List of PacketExtensions
  447.      */
  448.     public synchronized List<DataForm> getExtendedInfo() {
  449.         return CollectionUtil.newListWith(extendedInfos);
  450.     }

  451.     /**
  452.      * Removes the data form containing extended service discovery information
  453.      * from the information returned by this XMPP entity.<p>
  454.      *
  455.      * Since no stanza is actually sent to the server it is safe to perform this
  456.      * operation before logging to the server.
  457.      */
  458.     public synchronized void removeExtendedInfo() {
  459.         int extendedInfosCount = extendedInfos.size();
  460.         extendedInfos.clear();
  461.         if (extendedInfosCount > 0) {
  462.             // Notify others of a state change of SDM. In order to keep the state consistent, this
  463.             // method is synchronized
  464.             renewEntityCapsVersion();
  465.         }
  466.     }

  467.     /**
  468.      * Returns the discovered information of a given XMPP entity addressed by its JID.
  469.      * Use null as entityID to query the server
  470.      *
  471.      * @param entityID the address of the XMPP entity or null.
  472.      * @return the discovered information.
  473.      * @throws XMPPErrorException if there was an XMPP error returned.
  474.      * @throws NoResponseException if there was no response from the remote entity.
  475.      * @throws NotConnectedException if the XMPP connection is not connected.
  476.      * @throws InterruptedException if the calling thread was interrupted.
  477.      */
  478.     public DiscoverInfo discoverInfo(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  479.         if (entityID == null)
  480.             return discoverInfo(null, null);

  481.         synchronized (discoInfoLookupShortcutMechanisms) {
  482.             for (DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism : discoInfoLookupShortcutMechanisms) {
  483.                 DiscoverInfo info = discoInfoLookupShortcutMechanism.getDiscoverInfoByUser(this, entityID);
  484.                 if (info != null) {
  485.                     // We were able to retrieve the information from Entity Caps and
  486.                     // avoided a disco request, hurray!
  487.                     return info;
  488.                 }
  489.             }
  490.         }

  491.         // Last resort: Standard discovery.
  492.         return discoverInfo(entityID, null);
  493.     }

  494.     /**
  495.      * Returns the discovered information of a given XMPP entity addressed by its JID and
  496.      * note attribute. Use this message only when trying to query information which is not
  497.      * directly addressable.
  498.      *
  499.      * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a>
  500.      * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a>
  501.      *
  502.      * @param entityID the address of the XMPP entity.
  503.      * @param node the optional attribute that supplements the 'jid' attribute.
  504.      * @return the discovered information.
  505.      * @throws XMPPErrorException if the operation failed for some reason.
  506.      * @throws NoResponseException if there was no response from the server.
  507.      * @throws NotConnectedException if the XMPP connection is not connected.
  508.      * @throws InterruptedException if the calling thread was interrupted.
  509.      */
  510.     public DiscoverInfo discoverInfo(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  511.         XMPPConnection connection = connection();

  512.         // Discover the entity's info
  513.         DiscoverInfo discoInfoRequest = DiscoverInfo.builder(connection)
  514.                 .to(entityID)
  515.                 .setNode(node)
  516.                 .build();

  517.         Stanza result = connection.sendIqRequestAndWaitForResponse(discoInfoRequest);

  518.         return (DiscoverInfo) result;
  519.     }

  520.     /**
  521.      * Returns the discovered items of a given XMPP entity addressed by its JID.
  522.      *
  523.      * @param entityID the address of the XMPP entity.
  524.      * @return the discovered information.
  525.      * @throws XMPPErrorException if the operation failed for some reason.
  526.      * @throws NoResponseException if there was no response from the server.
  527.      * @throws NotConnectedException if the XMPP connection is not connected.
  528.      * @throws InterruptedException if the calling thread was interrupted.
  529.      */
  530.     public DiscoverItems discoverItems(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
  531.         return discoverItems(entityID, null);
  532.     }

  533.     /**
  534.      * Returns the discovered items of a given XMPP entity addressed by its JID and
  535.      * note attribute. Use this message only when trying to query information which is not
  536.      * directly addressable.
  537.      *
  538.      * @param entityID the address of the XMPP entity.
  539.      * @param node the optional attribute that supplements the 'jid' attribute.
  540.      * @return the discovered items.
  541.      * @throws XMPPErrorException if the operation failed for some reason.
  542.      * @throws NoResponseException if there was no response from the server.
  543.      * @throws NotConnectedException if the XMPP connection is not connected.
  544.      * @throws InterruptedException if the calling thread was interrupted.
  545.      */
  546.     public DiscoverItems discoverItems(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  547.         // Discover the entity's items
  548.         DiscoverItems disco = new DiscoverItems();
  549.         disco.setType(IQ.Type.get);
  550.         disco.setTo(entityID);
  551.         disco.setNode(node);

  552.         Stanza result = connection().sendIqRequestAndWaitForResponse(disco);
  553.         return (DiscoverItems) result;
  554.     }

  555.     /**
  556.      * Returns true if the server supports the given feature.
  557.      *
  558.      * @param feature TODO javadoc me please
  559.      * @return true if the server supports the given feature.
  560.      * @throws NoResponseException if there was no response from the remote entity.
  561.      * @throws XMPPErrorException if there was an XMPP error returned.
  562.      * @throws NotConnectedException if the XMPP connection is not connected.
  563.      * @throws InterruptedException if the calling thread was interrupted.
  564.      * @since 4.1
  565.      */
  566.     public boolean serverSupportsFeature(CharSequence feature) throws NoResponseException, XMPPErrorException,
  567.                     NotConnectedException, InterruptedException {
  568.         return serverSupportsFeatures(feature);
  569.     }

  570.     public boolean serverSupportsFeatures(CharSequence... features) throws NoResponseException,
  571.                     XMPPErrorException, NotConnectedException, InterruptedException {
  572.         return serverSupportsFeatures(Arrays.asList(features));
  573.     }

  574.     public boolean serverSupportsFeatures(Collection<? extends CharSequence> features)
  575.                     throws NoResponseException, XMPPErrorException, NotConnectedException,
  576.                     InterruptedException {
  577.         return supportsFeatures(connection().getXMPPServiceDomain(), features);
  578.     }

  579.     /**
  580.      * Check if the given features are supported by the connection account. This means that the discovery information
  581.      * lookup will be performed on the bare JID of the connection managed by this ServiceDiscoveryManager.
  582.      *
  583.      * @param features the features to check
  584.      * @return <code>true</code> if all features are supported by the connection account, <code>false</code> otherwise
  585.      * @throws NoResponseException if there was no response from the remote entity.
  586.      * @throws XMPPErrorException if there was an XMPP error returned.
  587.      * @throws NotConnectedException if the XMPP connection is not connected.
  588.      * @throws InterruptedException if the calling thread was interrupted.
  589.      * @since 4.2.2
  590.      */
  591.     public boolean accountSupportsFeatures(CharSequence... features)
  592.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  593.         return accountSupportsFeatures(Arrays.asList(features));
  594.     }

  595.     /**
  596.      * Check if the given collection of features are supported by the connection account. This means that the discovery
  597.      * information lookup will be performed on the bare JID of the connection managed by this ServiceDiscoveryManager.
  598.      *
  599.      * @param features a collection of features
  600.      * @return <code>true</code> if all features are supported by the connection account, <code>false</code> otherwise
  601.      * @throws NoResponseException if there was no response from the remote entity.
  602.      * @throws XMPPErrorException if there was an XMPP error returned.
  603.      * @throws NotConnectedException if the XMPP connection is not connected.
  604.      * @throws InterruptedException if the calling thread was interrupted.
  605.      * @since 4.2.2
  606.      */
  607.     public boolean accountSupportsFeatures(Collection<? extends CharSequence> features)
  608.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  609.         EntityBareJid accountJid = connection().getUser().asEntityBareJid();
  610.         return supportsFeatures(accountJid, features);
  611.     }

  612.     /**
  613.      * Queries the remote entity for it's features and returns true if the given feature is found.
  614.      *
  615.      * @param jid the JID of the remote entity
  616.      * @param feature TODO javadoc me please
  617.      * @return true if the entity supports the feature, false otherwise
  618.      * @throws XMPPErrorException if there was an XMPP error returned.
  619.      * @throws NoResponseException if there was no response from the remote entity.
  620.      * @throws NotConnectedException if the XMPP connection is not connected.
  621.      * @throws InterruptedException if the calling thread was interrupted.
  622.      */
  623.     public boolean supportsFeature(Jid jid, CharSequence feature) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  624.         return supportsFeatures(jid, feature);
  625.     }

  626.     public boolean supportsFeatures(Jid jid, CharSequence... features) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  627.         return supportsFeatures(jid, Arrays.asList(features));
  628.     }

  629.     public boolean supportsFeatures(Jid jid, Collection<? extends CharSequence> features) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  630.         DiscoverInfo result = discoverInfo(jid);
  631.         for (CharSequence feature : features) {
  632.             if (!result.containsFeature(feature)) {
  633.                 return false;
  634.             }
  635.         }
  636.         return true;
  637.     }

  638.     /**
  639.      * Create a cache to hold the 25 most recently lookup services for a given feature for a period
  640.      * of 24 hours.
  641.      */
  642.     private final Cache<String, List<DiscoverInfo>> services = new ExpirationCache<>(25,
  643.                     24 * 60 * 60 * 1000);

  644.     /**
  645.      * Find all services under the users service that provide a given feature.
  646.      *
  647.      * @param feature the feature to search for
  648.      * @param stopOnFirst if true, stop searching after the first service was found
  649.      * @param useCache if true, query a cache first to avoid network I/O
  650.      * @return a possible empty list of services providing the given feature
  651.      * @throws NoResponseException if there was no response from the remote entity.
  652.      * @throws XMPPErrorException if there was an XMPP error returned.
  653.      * @throws NotConnectedException if the XMPP connection is not connected.
  654.      * @throws InterruptedException if the calling thread was interrupted.
  655.      */
  656.     public List<DiscoverInfo> findServicesDiscoverInfo(String feature, boolean stopOnFirst, boolean useCache)
  657.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  658.         return findServicesDiscoverInfo(feature, stopOnFirst, useCache, null);
  659.     }

  660.     /**
  661.      * Find all services under the users service that provide a given feature.
  662.      *
  663.      * @param feature the feature to search for
  664.      * @param stopOnFirst if true, stop searching after the first service was found
  665.      * @param useCache if true, query a cache first to avoid network I/O
  666.      * @param encounteredExceptions an optional map which will be filled with the exceptions encountered
  667.      * @return a possible empty list of services providing the given feature
  668.      * @throws NoResponseException if there was no response from the remote entity.
  669.      * @throws XMPPErrorException if there was an XMPP error returned.
  670.      * @throws NotConnectedException if the XMPP connection is not connected.
  671.      * @throws InterruptedException if the calling thread was interrupted.
  672.      * @since 4.2.2
  673.      */
  674.     public List<DiscoverInfo> findServicesDiscoverInfo(String feature, boolean stopOnFirst, boolean useCache, Map<? super Jid, Exception> encounteredExceptions)
  675.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  676.         DomainBareJid serviceName = connection().getXMPPServiceDomain();
  677.         return findServicesDiscoverInfo(serviceName, feature, stopOnFirst, useCache, encounteredExceptions);
  678.     }

  679.     /**
  680.      * Find all services under a given service that provide a given feature.
  681.      *
  682.      * @param serviceName the service to query
  683.      * @param feature the feature to search for
  684.      * @param stopOnFirst if true, stop searching after the first service was found
  685.      * @param useCache if true, query a cache first to avoid network I/O
  686.      * @param encounteredExceptions an optional map which will be filled with the exceptions encountered
  687.      * @return a possible empty list of services providing the given feature
  688.      * @throws NoResponseException if there was no response from the remote entity.
  689.      * @throws XMPPErrorException if there was an XMPP error returned.
  690.      * @throws NotConnectedException if the XMPP connection is not connected.
  691.      * @throws InterruptedException if the calling thread was interrupted.
  692.      * @since 4.3.0
  693.      */
  694.     public List<DiscoverInfo> findServicesDiscoverInfo(DomainBareJid serviceName, String feature, boolean stopOnFirst,
  695.                     boolean useCache, Map<? super Jid, Exception> encounteredExceptions)
  696.             throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  697.         List<DiscoverInfo> serviceDiscoInfo;
  698.         if (useCache) {
  699.             serviceDiscoInfo = services.lookup(feature);
  700.             if (serviceDiscoInfo != null) {
  701.                 return serviceDiscoInfo;
  702.             }
  703.         }
  704.         serviceDiscoInfo = new LinkedList<>();
  705.         // Send the disco packet to the server itself
  706.         DiscoverInfo info;
  707.         try {
  708.             info = discoverInfo(serviceName);
  709.         } catch (XMPPErrorException e) {
  710.             if (encounteredExceptions != null) {
  711.                 encounteredExceptions.put(serviceName, e);
  712.             }
  713.             return serviceDiscoInfo;
  714.         }
  715.         // Check if the server supports the feature
  716.         if (info.containsFeature(feature)) {
  717.             serviceDiscoInfo.add(info);
  718.             if (stopOnFirst) {
  719.                 if (useCache) {
  720.                     // Cache the discovered information
  721.                     services.put(feature, serviceDiscoInfo);
  722.                 }
  723.                 return serviceDiscoInfo;
  724.             }
  725.         }
  726.         DiscoverItems items;
  727.         try {
  728.             // Get the disco items and send the disco packet to each server item
  729.             items = discoverItems(serviceName);
  730.         } catch (XMPPErrorException e) {
  731.             if (encounteredExceptions != null) {
  732.                 encounteredExceptions.put(serviceName, e);
  733.             }
  734.             return serviceDiscoInfo;
  735.         }
  736.         for (DiscoverItems.Item item : items.getItems()) {
  737.             Jid address = item.getEntityID();
  738.             try {
  739.                 // TODO is it OK here in all cases to query without the node attribute?
  740.                 // MultipleRecipientManager queried initially also with the node attribute, but this
  741.                 // could be simply a fault instead of intentional.
  742.                 info = discoverInfo(address);
  743.             }
  744.             catch (XMPPErrorException | NoResponseException e) {
  745.                 if (encounteredExceptions != null) {
  746.                     encounteredExceptions.put(address, e);
  747.                 }
  748.                 continue;
  749.             }
  750.             if (info.containsFeature(feature)) {
  751.                 serviceDiscoInfo.add(info);
  752.                 if (stopOnFirst) {
  753.                     break;
  754.                 }
  755.             }
  756.         }
  757.         if (useCache) {
  758.             // Cache the discovered information
  759.             services.put(feature, serviceDiscoInfo);
  760.         }
  761.         return serviceDiscoInfo;
  762.     }

  763.     /**
  764.      * Find all services under the users service that provide a given feature.
  765.      *
  766.      * @param feature the feature to search for
  767.      * @param stopOnFirst if true, stop searching after the first service was found
  768.      * @param useCache if true, query a cache first to avoid network I/O
  769.      * @return a possible empty list of services providing the given feature
  770.      * @throws NoResponseException if there was no response from the remote entity.
  771.      * @throws XMPPErrorException if there was an XMPP error returned.
  772.      * @throws NotConnectedException if the XMPP connection is not connected.
  773.      * @throws InterruptedException if the calling thread was interrupted.
  774.      */
  775.     public List<DomainBareJid> findServices(String feature, boolean stopOnFirst, boolean useCache) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  776.         List<DiscoverInfo> services = findServicesDiscoverInfo(feature, stopOnFirst, useCache);
  777.         List<DomainBareJid> res = new ArrayList<>(services.size());
  778.         for (DiscoverInfo info : services) {
  779.             res.add(info.getFrom().asDomainBareJid());
  780.         }
  781.         return res;
  782.     }

  783.     public DomainBareJid findService(String feature, boolean useCache, String category, String type)
  784.                     throws NoResponseException, XMPPErrorException, NotConnectedException,
  785.                     InterruptedException {
  786.         boolean noCategory = StringUtils.isNullOrEmpty(category);
  787.         boolean noType = StringUtils.isNullOrEmpty(type);
  788.         if (noType != noCategory) {
  789.             throw new IllegalArgumentException("Must specify either both, category and type, or none");
  790.         }

  791.         List<DiscoverInfo> services = findServicesDiscoverInfo(feature, false, useCache);
  792.         if (services.isEmpty()) {
  793.             return null;
  794.         }

  795.         if (!noCategory && !noType) {
  796.             for (DiscoverInfo info : services) {
  797.                 if (info.hasIdentity(category, type)) {
  798.                     return info.getFrom().asDomainBareJid();
  799.                 }
  800.             }
  801.         }

  802.         return services.get(0).getFrom().asDomainBareJid();
  803.     }

  804.     public DomainBareJid findService(String feature, boolean useCache) throws NoResponseException,
  805.                     XMPPErrorException, NotConnectedException, InterruptedException {
  806.         return findService(feature, useCache, null, null);
  807.     }

  808.     public boolean addEntityCapabilitiesChangedListener(EntityCapabilitiesChangedListener entityCapabilitiesChangedListener) {
  809.         return entityCapabilitiesChangedListeners.add(entityCapabilitiesChangedListener);
  810.     }

  811.     public boolean removeEntityCapabilitiesChangedListener(EntityCapabilitiesChangedListener entityCapabilitiesChangedListener) {
  812.         return entityCapabilitiesChangedListeners.remove(entityCapabilitiesChangedListener);
  813.     }

  814.     private static final int RENEW_ENTITY_CAPS_DELAY_MILLIS = 25;

  815.     private ScheduledAction renewEntityCapsScheduledAction;

  816.     private final AtomicInteger renewEntityCapsPerformed = new AtomicInteger();
  817.     private int renewEntityCapsRequested = 0;
  818.     private int scheduledRenewEntityCapsAvoided = 0;

  819.     /**
  820.      * Notify the {@link EntityCapabilitiesChangedListener} about changed capabilities.
  821.      */
  822.     private synchronized void renewEntityCapsVersion() {
  823.         if (entityCapabilitiesChangedListeners.isEmpty()) {
  824.             return;
  825.         }

  826.         renewEntityCapsRequested++;
  827.         if (renewEntityCapsScheduledAction != null) {
  828.             boolean canceled = renewEntityCapsScheduledAction.cancel();
  829.             if (canceled) {
  830.                 scheduledRenewEntityCapsAvoided++;
  831.             }
  832.         }

  833.         renewEntityCapsScheduledAction = scheduleBlocking(() -> {
  834.             final XMPPConnection connection = connection();
  835.             if (connection == null) {
  836.                 return;
  837.             }

  838.             renewEntityCapsPerformed.incrementAndGet();

  839.             DiscoverInfoBuilder discoverInfoBuilder = DiscoverInfo.builder("synthetized-disco-info-response")
  840.                             .ofType(IQ.Type.result);
  841.             addDiscoverInfoTo(discoverInfoBuilder);
  842.             DiscoverInfo synthesizedDiscoveryInfo = discoverInfoBuilder.build();

  843.             for (EntityCapabilitiesChangedListener entityCapabilitiesChangedListener : entityCapabilitiesChangedListeners) {
  844.                 entityCapabilitiesChangedListener.onEntityCapabilitiesChanged(synthesizedDiscoveryInfo);
  845.             }

  846.             // Re-send the last sent presence, and let the stanza interceptor
  847.             // add a <c/> node to it.
  848.             // See http://xmpp.org/extensions/xep-0115.html#advertise
  849.             // We only send a presence packet if there was already one send
  850.             // to respect ConnectionConfiguration.isSendPresence()
  851.             final Presence presenceSend = this.presenceSend;
  852.             if (connection.isAuthenticated() && presenceSend != null) {
  853.                 Presence presence = presenceSend.asBuilder(connection).build();
  854.                 try {
  855.                     connection.sendStanza(presence);
  856.                 }
  857.                 catch (InterruptedException | NotConnectedException e) {
  858.                     LOGGER.log(Level.WARNING, "Could could not update presence with caps info", e);
  859.                 }
  860.             }
  861.         }, RENEW_ENTITY_CAPS_DELAY_MILLIS, TimeUnit.MILLISECONDS);
  862.     }

  863.     public static void addDiscoInfoLookupShortcutMechanism(DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism) {
  864.         synchronized (discoInfoLookupShortcutMechanisms) {
  865.             discoInfoLookupShortcutMechanisms.add(discoInfoLookupShortcutMechanism);
  866.             Collections.sort(discoInfoLookupShortcutMechanisms);
  867.         }
  868.     }

  869.     public static void removeDiscoInfoLookupShortcutMechanism(DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism) {
  870.         synchronized (discoInfoLookupShortcutMechanisms) {
  871.             discoInfoLookupShortcutMechanisms.remove(discoInfoLookupShortcutMechanism);
  872.         }
  873.     }

  874.     public synchronized Stats getStats() {
  875.         return new Stats(this);
  876.     }

  877.     public static final class Stats extends AbstractStats {

  878.         public final int renewEntityCapsRequested;
  879.         public final int renewEntityCapsPerformed;
  880.         public final int scheduledRenewEntityCapsAvoided;

  881.         private Stats(ServiceDiscoveryManager serviceDiscoveryManager) {
  882.             renewEntityCapsRequested = serviceDiscoveryManager.renewEntityCapsRequested;
  883.             renewEntityCapsPerformed = serviceDiscoveryManager.renewEntityCapsPerformed.get();
  884.             scheduledRenewEntityCapsAvoided = serviceDiscoveryManager.scheduledRenewEntityCapsAvoided;
  885.         }

  886.         @Override
  887.         public void appendStatsTo(ExtendedAppendable appendable) throws IOException {
  888.             StringUtils.appendHeading(appendable, "ServiceDiscoveryManager stats", '#').append('\n');
  889.             appendable.append("renew-entitycaps-requested: ").append(renewEntityCapsRequested).append('\n');
  890.             appendable.append("renew-entitycaps-performed: ").append(renewEntityCapsPerformed).append('\n');
  891.             appendable.append("scheduled-renew-entitycaps-avoided: ").append(scheduledRenewEntityCapsAvoided).append('\n');
  892.         }

  893.     }
  894. }