ServiceDiscoveryManager.java

  1. /**
  2.  *
  3.  * Copyright 2003-2007 Jive Software.
  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 org.jivesoftware.smack.SmackException.NoResponseException;
  19. import org.jivesoftware.smack.SmackException.NotConnectedException;
  20. import org.jivesoftware.smack.XMPPConnection;
  21. import org.jivesoftware.smack.ConnectionCreationListener;
  22. import org.jivesoftware.smack.Manager;
  23. import org.jivesoftware.smack.XMPPConnectionRegistry;
  24. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  25. import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
  26. import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode;
  27. import org.jivesoftware.smack.packet.IQ;
  28. import org.jivesoftware.smack.packet.Stanza;
  29. import org.jivesoftware.smack.packet.ExtensionElement;
  30. import org.jivesoftware.smack.packet.XMPPError;
  31. import org.jivesoftware.smackx.caps.EntityCapsManager;
  32. import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
  33. import org.jivesoftware.smackx.disco.packet.DiscoverItems;
  34. import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity;
  35. import org.jivesoftware.smackx.xdata.packet.DataForm;
  36. import org.jxmpp.jid.DomainBareJid;
  37. import org.jxmpp.jid.Jid;
  38. import org.jxmpp.util.cache.Cache;
  39. import org.jxmpp.util.cache.ExpirationCache;

  40. import java.util.ArrayList;
  41. import java.util.Collections;
  42. import java.util.HashSet;
  43. import java.util.LinkedList;
  44. import java.util.List;
  45. import java.util.Map;
  46. import java.util.Set;
  47. import java.util.WeakHashMap;
  48. import java.util.concurrent.ConcurrentHashMap;
  49. import java.util.logging.Level;
  50. import java.util.logging.Logger;

  51. /**
  52.  * Manages discovery of services in XMPP entities. This class provides:
  53.  * <ol>
  54.  * <li>A registry of supported features in this XMPP entity.
  55.  * <li>Automatic response when this XMPP entity is queried for information.
  56.  * <li>Ability to discover items and information of remote XMPP entities.
  57.  * <li>Ability to publish publicly available items.
  58.  * </ol>  
  59.  *
  60.  * @author Gaston Dombiak
  61.  */
  62. public class ServiceDiscoveryManager extends Manager {

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

  64.     private static final String DEFAULT_IDENTITY_NAME = "Smack";
  65.     private static final String DEFAULT_IDENTITY_CATEGORY = "client";
  66.     private static final String DEFAULT_IDENTITY_TYPE = "pc";

  67.     private static DiscoverInfo.Identity defaultIdentity = new Identity(DEFAULT_IDENTITY_CATEGORY,
  68.             DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE);

  69.     private Set<DiscoverInfo.Identity> identities = new HashSet<DiscoverInfo.Identity>();
  70.     private DiscoverInfo.Identity identity = defaultIdentity;

  71.     private EntityCapsManager capsManager;

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

  73.     private final Set<String> features = new HashSet<String>();
  74.     private DataForm extendedInfo = null;
  75.     private Map<String, NodeInformationProvider> nodeInformationProviders =
  76.             new ConcurrentHashMap<String, NodeInformationProvider>();

  77.     // Create a new ServiceDiscoveryManager on every established connection
  78.     static {
  79.         XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
  80.             public void connectionCreated(XMPPConnection connection) {
  81.                 getInstanceFor(connection);
  82.             }
  83.         });
  84.     }

  85.     /**
  86.      * Set the default identity all new connections will have. If unchanged the default identity is an
  87.      * identity where category is set to 'client', type is set to 'pc' and name is set to 'Smack'.
  88.      *
  89.      * @param identity
  90.      */
  91.     public static void setDefaultIdentity(DiscoverInfo.Identity identity) {
  92.         defaultIdentity = identity;
  93.     }

  94.     /**
  95.      * Creates a new ServiceDiscoveryManager for a given XMPPConnection. This means that the
  96.      * service manager will respond to any service discovery request that the connection may
  97.      * receive.
  98.      *
  99.      * @param connection the connection to which a ServiceDiscoveryManager is going to be created.
  100.      */
  101.     private ServiceDiscoveryManager(XMPPConnection connection) {
  102.         super(connection);

  103.         addFeature(DiscoverInfo.NAMESPACE);
  104.         addFeature(DiscoverItems.NAMESPACE);

  105.         // Listen for disco#items requests and answer with an empty result        
  106.         connection.registerIQRequestHandler(new AbstractIqRequestHandler(DiscoverItems.ELEMENT, DiscoverItems.NAMESPACE, IQ.Type.get, Mode.async) {
  107.             @Override
  108.             public IQ handleIQRequest(IQ iqRequest) {
  109.                 DiscoverItems discoverItems = (DiscoverItems) iqRequest;
  110.                 DiscoverItems response = new DiscoverItems();
  111.                 response.setType(IQ.Type.result);
  112.                 response.setTo(discoverItems.getFrom());
  113.                 response.setStanzaId(discoverItems.getStanzaId());
  114.                 response.setNode(discoverItems.getNode());

  115.                 // Add the defined items related to the requested node. Look for
  116.                 // the NodeInformationProvider associated with the requested node.
  117.                 NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverItems.getNode());
  118.                 if (nodeInformationProvider != null) {
  119.                     // Specified node was found, add node items
  120.                     response.addItems(nodeInformationProvider.getNodeItems());
  121.                     // Add packet extensions
  122.                     response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
  123.                 } else if(discoverItems.getNode() != null) {
  124.                     // Return <item-not-found/> error since client doesn't contain
  125.                     // the specified node
  126.                     response.setType(IQ.Type.error);
  127.                     response.setError(new XMPPError(XMPPError.Condition.item_not_found));
  128.                 }
  129.                 return response;
  130.             }
  131.         });

  132.         // Listen for disco#info requests and answer the client's supported features
  133.         // To add a new feature as supported use the #addFeature message        
  134.         connection.registerIQRequestHandler(new AbstractIqRequestHandler(DiscoverInfo.ELEMENT, DiscoverInfo.NAMESPACE, IQ.Type.get, Mode.async) {
  135.             @Override
  136.             public IQ handleIQRequest(IQ iqRequest) {
  137.                 DiscoverInfo discoverInfo = (DiscoverInfo) iqRequest;
  138.                 // Answer the client's supported features if the request is of the GET type
  139.                 DiscoverInfo response = new DiscoverInfo();
  140.                 response.setType(IQ.Type.result);
  141.                 response.setTo(discoverInfo.getFrom());
  142.                 response.setStanzaId(discoverInfo.getStanzaId());
  143.                 response.setNode(discoverInfo.getNode());
  144.                 // Add the client's identity and features only if "node" is null
  145.                 // and if the request was not send to a node. If Entity Caps are
  146.                 // enabled the client's identity and features are may also added
  147.                 // if the right node is chosen
  148.                 if (discoverInfo.getNode() == null) {
  149.                     addDiscoverInfoTo(response);
  150.                 } else {
  151.                     // Disco#info was sent to a node. Check if we have information of the
  152.                     // specified node
  153.                     NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverInfo.getNode());
  154.                     if (nodeInformationProvider != null) {
  155.                         // Node was found. Add node features
  156.                         response.addFeatures(nodeInformationProvider.getNodeFeatures());
  157.                         // Add node identities
  158.                         response.addIdentities(nodeInformationProvider.getNodeIdentities());
  159.                         // Add packet extensions
  160.                         response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
  161.                     } else {
  162.                         // Return <item-not-found/> error since specified node was not found
  163.                         response.setType(IQ.Type.error);
  164.                         response.setError(new XMPPError(XMPPError.Condition.item_not_found));
  165.                     }
  166.                 }
  167.                 return response;
  168.             }
  169.         });
  170.     }

  171.     /**
  172.      * Returns the name of the client that will be returned when asked for the client identity
  173.      * in a disco request. The name could be any value you need to identity this client.
  174.      *
  175.      * @return the name of the client that will be returned when asked for the client identity
  176.      *          in a disco request.
  177.      */
  178.     public String getIdentityName() {
  179.         return identity.getName();
  180.     }

  181.     /**
  182.      * Sets the default identity the client will report.
  183.      *
  184.      * @param identity
  185.      */
  186.     public void setIdentity(Identity identity) {
  187.         if (identity == null) throw new IllegalArgumentException("Identity can not be null");
  188.         this.identity = identity;
  189.         renewEntityCapsVersion();
  190.     }

  191.     /**
  192.      * Return the default identity of the client.
  193.      *
  194.      * @return the default identity.
  195.      */
  196.     public Identity getIdentity() {
  197.         return identity;
  198.     }

  199.     /**
  200.      * Returns the type of client that will be returned when asked for the client identity in a
  201.      * disco request. The valid types are defined by the category client. Follow this link to learn
  202.      * the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>.
  203.      *
  204.      * @return the type of client that will be returned when asked for the client identity in a
  205.      *          disco request.
  206.      */
  207.     public String getIdentityType() {
  208.         return identity.getType();
  209.     }

  210.     /**
  211.      * Add an further identity to the client.
  212.      *
  213.      * @param identity
  214.      */
  215.     public void addIdentity(DiscoverInfo.Identity identity) {
  216.         identities.add(identity);
  217.         renewEntityCapsVersion();
  218.     }

  219.     /**
  220.      * Remove an identity from the client. Note that the client needs at least one identity, the default identity, which
  221.      * can not be removed.
  222.      *
  223.      * @param identity
  224.      * @return true, if successful. Otherwise the default identity was given.
  225.      */
  226.     public boolean removeIdentity(DiscoverInfo.Identity identity) {
  227.         if (identity.equals(this.identity)) return false;
  228.         identities.remove(identity);
  229.         renewEntityCapsVersion();
  230.         return true;
  231.     }

  232.     /**
  233.      * Returns all identities of this client as unmodifiable Collection
  234.      *
  235.      * @return all identies as set
  236.      */
  237.     public Set<DiscoverInfo.Identity> getIdentities() {
  238.         Set<Identity> res = new HashSet<Identity>(identities);
  239.         // Add the default identity that must exist
  240.         res.add(defaultIdentity);
  241.         return Collections.unmodifiableSet(res);
  242.     }

  243.     /**
  244.      * Returns the ServiceDiscoveryManager instance associated with a given XMPPConnection.
  245.      *
  246.      * @param connection the connection used to look for the proper ServiceDiscoveryManager.
  247.      * @return the ServiceDiscoveryManager associated with a given XMPPConnection.
  248.      */
  249.     public static synchronized ServiceDiscoveryManager getInstanceFor(XMPPConnection connection) {
  250.         ServiceDiscoveryManager sdm = instances.get(connection);
  251.         if (sdm == null) {
  252.             sdm = new ServiceDiscoveryManager(connection);
  253.             // Register the new instance and associate it with the connection
  254.             instances.put(connection, sdm);
  255.         }
  256.         return sdm;
  257.     }

  258.     /**
  259.      * Add discover info response data.
  260.      *
  261.      * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol; Example 2</a>
  262.      *
  263.      * @param response the discover info response packet
  264.      */
  265.     public void addDiscoverInfoTo(DiscoverInfo response) {
  266.         // First add the identities of the connection
  267.         response.addIdentities(getIdentities());

  268.         // Add the registered features to the response
  269.         synchronized (features) {
  270.             for (String feature : getFeatures()) {
  271.                 response.addFeature(feature);
  272.             }
  273.             response.addExtension(extendedInfo);
  274.         }
  275.     }

  276.     /**
  277.      * Returns the NodeInformationProvider responsible for providing information
  278.      * (ie items) related to a given node or <tt>null</null> if none.<p>
  279.      *
  280.      * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
  281.      * NodeInformationProvider will provide information about the rooms where the user has joined.
  282.      *
  283.      * @param node the node that contains items associated with an entity not addressable as a JID.
  284.      * @return the NodeInformationProvider responsible for providing information related
  285.      * to a given node.
  286.      */
  287.     private NodeInformationProvider getNodeInformationProvider(String node) {
  288.         if (node == null) {
  289.             return null;
  290.         }
  291.         return nodeInformationProviders.get(node);
  292.     }

  293.     /**
  294.      * Sets the NodeInformationProvider responsible for providing information
  295.      * (ie items) related to a given node. Every time this client receives a disco request
  296.      * regarding the items of a given node, the provider associated to that node will be the
  297.      * responsible for providing the requested information.<p>
  298.      *
  299.      * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
  300.      * NodeInformationProvider will provide information about the rooms where the user has joined.
  301.      *
  302.      * @param node the node whose items will be provided by the NodeInformationProvider.
  303.      * @param listener the NodeInformationProvider responsible for providing items related
  304.      *      to the node.
  305.      */
  306.     public void setNodeInformationProvider(String node, NodeInformationProvider listener) {
  307.         nodeInformationProviders.put(node, listener);
  308.     }

  309.     /**
  310.      * Removes the NodeInformationProvider responsible for providing information
  311.      * (ie items) related to a given node. This means that no more information will be
  312.      * available for the specified node.
  313.      *
  314.      * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
  315.      * NodeInformationProvider will provide information about the rooms where the user has joined.
  316.      *
  317.      * @param node the node to remove the associated NodeInformationProvider.
  318.      */
  319.     public void removeNodeInformationProvider(String node) {
  320.         nodeInformationProviders.remove(node);
  321.     }

  322.     /**
  323.      * Returns the supported features by this XMPP entity.
  324.      * <p>
  325.      * The result is a copied modifiable list of the original features.
  326.      * </p>
  327.      *
  328.      * @return a List of the supported features by this XMPP entity.
  329.      */
  330.     public List<String> getFeatures() {
  331.         synchronized (features) {
  332.             return new ArrayList<String>(features);
  333.         }
  334.     }

  335.     /**
  336.      * Registers that a new feature is supported by this XMPP entity. When this client is
  337.      * queried for its information the registered features will be answered.<p>
  338.      *
  339.      * Since no packet is actually sent to the server it is safe to perform this operation
  340.      * before logging to the server. In fact, you may want to configure the supported features
  341.      * before logging to the server so that the information is already available if it is required
  342.      * upon login.
  343.      *
  344.      * @param feature the feature to register as supported.
  345.      */
  346.     public void addFeature(String feature) {
  347.         synchronized (features) {
  348.             if (!features.contains(feature)) {
  349.                 features.add(feature);
  350.                 renewEntityCapsVersion();
  351.             }
  352.         }
  353.     }

  354.     /**
  355.      * Removes the specified feature from the supported features by this XMPP entity.<p>
  356.      *
  357.      * Since no packet is actually sent to the server it is safe to perform this operation
  358.      * before logging to the server.
  359.      *
  360.      * @param feature the feature to remove from the supported features.
  361.      */
  362.     public void removeFeature(String feature) {
  363.         synchronized (features) {
  364.             features.remove(feature);
  365.             renewEntityCapsVersion();
  366.         }
  367.     }

  368.     /**
  369.      * Returns true if the specified feature is registered in the ServiceDiscoveryManager.
  370.      *
  371.      * @param feature the feature to look for.
  372.      * @return a boolean indicating if the specified featured is registered or not.
  373.      */
  374.     public boolean includesFeature(String feature) {
  375.         synchronized (features) {
  376.             return features.contains(feature);
  377.         }
  378.     }

  379.     /**
  380.      * Registers extended discovery information of this XMPP entity. When this
  381.      * client is queried for its information this data form will be returned as
  382.      * specified by XEP-0128.
  383.      * <p>
  384.      *
  385.      * Since no packet is actually sent to the server it is safe to perform this
  386.      * operation before logging to the server. In fact, you may want to
  387.      * configure the extended info before logging to the server so that the
  388.      * information is already available if it is required upon login.
  389.      *
  390.      * @param info
  391.      *            the data form that contains the extend service discovery
  392.      *            information.
  393.      */
  394.     public void setExtendedInfo(DataForm info) {
  395.       extendedInfo = info;
  396.       renewEntityCapsVersion();
  397.     }

  398.     /**
  399.      * Returns the data form that is set as extended information for this Service Discovery instance (XEP-0128)
  400.      *
  401.      * @see <a href="http://xmpp.org/extensions/xep-0128.html">XEP-128: Service Discovery Extensions</a>
  402.      * @return the data form
  403.      */
  404.     public DataForm getExtendedInfo() {
  405.         return extendedInfo;
  406.     }

  407.     /**
  408.      * Returns the data form as List of PacketExtensions, or null if no data form is set.
  409.      * This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider)
  410.      *
  411.      * @return the data form as List of PacketExtensions
  412.      */
  413.     public List<ExtensionElement> getExtendedInfoAsList() {
  414.         List<ExtensionElement> res = null;
  415.         if (extendedInfo != null) {
  416.             res = new ArrayList<ExtensionElement>(1);
  417.             res.add(extendedInfo);
  418.         }
  419.         return res;
  420.     }

  421.     /**
  422.      * Removes the data form containing extended service discovery information
  423.      * from the information returned by this XMPP entity.<p>
  424.      *
  425.      * Since no packet is actually sent to the server it is safe to perform this
  426.      * operation before logging to the server.
  427.      */
  428.     public void removeExtendedInfo() {
  429.        extendedInfo = null;
  430.        renewEntityCapsVersion();
  431.     }

  432.     /**
  433.      * Returns the discovered information of a given XMPP entity addressed by its JID.
  434.      * Use null as entityID to query the server
  435.      *
  436.      * @param entityID the address of the XMPP entity or null.
  437.      * @return the discovered information.
  438.      * @throws XMPPErrorException
  439.      * @throws NoResponseException
  440.      * @throws NotConnectedException
  441.      * @throws InterruptedException
  442.      */
  443.     public DiscoverInfo discoverInfo(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  444.         if (entityID == null)
  445.             return discoverInfo(null, null);

  446.         // Check if the have it cached in the Entity Capabilities Manager
  447.         DiscoverInfo info = EntityCapsManager.getDiscoverInfoByUser(entityID);

  448.         if (info != null) {
  449.             // We were able to retrieve the information from Entity Caps and
  450.             // avoided a disco request, hurray!
  451.             return info;
  452.         }

  453.         // Try to get the newest node#version if it's known, otherwise null is
  454.         // returned
  455.         EntityCapsManager.NodeVerHash nvh = EntityCapsManager.getNodeVerHashByJid(entityID);

  456.         // Discover by requesting the information from the remote entity
  457.         // Note that wee need to use NodeVer as argument for Node if it exists
  458.         info = discoverInfo(entityID, nvh != null ? nvh.getNodeVer() : null);

  459.         // If the node version is known, store the new entry.
  460.         if (nvh != null) {
  461.             if (EntityCapsManager.verifyDiscoverInfoVersion(nvh.getVer(), nvh.getHash(), info))
  462.                 EntityCapsManager.addDiscoverInfoByNode(nvh.getNodeVer(), info);
  463.         }

  464.         return info;
  465.     }

  466.     /**
  467.      * Returns the discovered information of a given XMPP entity addressed by its JID and
  468.      * note attribute. Use this message only when trying to query information which is not
  469.      * directly addressable.
  470.      *
  471.      * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a>
  472.      * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a>
  473.      *
  474.      * @param entityID the address of the XMPP entity.
  475.      * @param node the optional attribute that supplements the 'jid' attribute.
  476.      * @return the discovered information.
  477.      * @throws XMPPErrorException if the operation failed for some reason.
  478.      * @throws NoResponseException if there was no response from the server.
  479.      * @throws NotConnectedException
  480.      * @throws InterruptedException
  481.      */
  482.     public DiscoverInfo discoverInfo(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  483.         // Discover the entity's info
  484.         DiscoverInfo disco = new DiscoverInfo();
  485.         disco.setType(IQ.Type.get);
  486.         disco.setTo(entityID);
  487.         disco.setNode(node);

  488.         Stanza result = connection().createPacketCollectorAndSend(disco).nextResultOrThrow();

  489.         return (DiscoverInfo) result;
  490.     }

  491.     /**
  492.      * Returns the discovered items of a given XMPP entity addressed by its JID.
  493.      *
  494.      * @param entityID the address of the XMPP entity.
  495.      * @return the discovered information.
  496.      * @throws XMPPErrorException if the operation failed for some reason.
  497.      * @throws NoResponseException if there was no response from the server.
  498.      * @throws NotConnectedException
  499.      * @throws InterruptedException
  500.      */
  501.     public DiscoverItems discoverItems(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
  502.         return discoverItems(entityID, null);
  503.     }

  504.     /**
  505.      * Returns the discovered items of a given XMPP entity addressed by its JID and
  506.      * note attribute. Use this message only when trying to query information which is not
  507.      * directly addressable.
  508.      *
  509.      * @param entityID the address of the XMPP entity.
  510.      * @param node the optional attribute that supplements the 'jid' attribute.
  511.      * @return the discovered items.
  512.      * @throws XMPPErrorException if the operation failed for some reason.
  513.      * @throws NoResponseException if there was no response from the server.
  514.      * @throws NotConnectedException
  515.      * @throws InterruptedException
  516.      */
  517.     public DiscoverItems discoverItems(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  518.         // Discover the entity's items
  519.         DiscoverItems disco = new DiscoverItems();
  520.         disco.setType(IQ.Type.get);
  521.         disco.setTo(entityID);
  522.         disco.setNode(node);

  523.         Stanza result = connection().createPacketCollectorAndSend(disco).nextResultOrThrow();
  524.         return (DiscoverItems) result;
  525.     }

  526.     /**
  527.      * Returns true if the server supports publishing of items. A client may wish to publish items
  528.      * to the server so that the server can provide items associated to the client. These items will
  529.      * be returned by the server whenever the server receives a disco request targeted to the bare
  530.      * address of the client (i.e. user@host.com).
  531.      *
  532.      * @param entityID the address of the XMPP entity.
  533.      * @return true if the server supports publishing of items.
  534.      * @throws XMPPErrorException
  535.      * @throws NoResponseException
  536.      * @throws NotConnectedException
  537.      * @throws InterruptedException
  538.      */
  539.     public boolean canPublishItems(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  540.         DiscoverInfo info = discoverInfo(entityID);
  541.         return canPublishItems(info);
  542.      }

  543.      /**
  544.       * Returns true if the server supports publishing of items. A client may wish to publish items
  545.       * to the server so that the server can provide items associated to the client. These items will
  546.       * be returned by the server whenever the server receives a disco request targeted to the bare
  547.       * address of the client (i.e. user@host.com).
  548.       *
  549.       * @param info the discover info packet to check.
  550.       * @return true if the server supports publishing of items.
  551.       */
  552.      public static boolean canPublishItems(DiscoverInfo info) {
  553.          return info.containsFeature("http://jabber.org/protocol/disco#publish");
  554.      }

  555.     /**
  556.      * Publishes new items to a parent entity. The item elements to publish MUST have at least
  557.      * a 'jid' attribute specifying the Entity ID of the item, and an action attribute which
  558.      * specifies the action being taken for that item. Possible action values are: "update" and
  559.      * "remove".
  560.      *
  561.      * @param entityID the address of the XMPP entity.
  562.      * @param discoverItems the DiscoveryItems to publish.
  563.      * @throws XMPPErrorException
  564.      * @throws NoResponseException
  565.      * @throws NotConnectedException
  566.      * @throws InterruptedException
  567.      */
  568.     public void publishItems(Jid entityID, DiscoverItems discoverItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  569.         publishItems(entityID, null, discoverItems);
  570.     }

  571.     /**
  572.      * Publishes new items to a parent entity and node. The item elements to publish MUST have at
  573.      * least a 'jid' attribute specifying the Entity ID of the item, and an action attribute which
  574.      * specifies the action being taken for that item. Possible action values are: "update" and
  575.      * "remove".
  576.      *
  577.      * @param entityID the address of the XMPP entity.
  578.      * @param node the attribute that supplements the 'jid' attribute.
  579.      * @param discoverItems the DiscoveryItems to publish.
  580.      * @throws XMPPErrorException if the operation failed for some reason.
  581.      * @throws NoResponseException if there was no response from the server.
  582.      * @throws NotConnectedException
  583.      * @throws InterruptedException
  584.      */
  585.     public void publishItems(Jid entityID, String node, DiscoverItems discoverItems) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException
  586.             {
  587.         discoverItems.setType(IQ.Type.set);
  588.         discoverItems.setTo(entityID);
  589.         discoverItems.setNode(node);

  590.         connection().createPacketCollectorAndSend(discoverItems).nextResultOrThrow();
  591.     }

  592.     /**
  593.      * Returns true if the server supports the given feature.
  594.      *
  595.      * @param feature
  596.      * @return true if the server supports the given feature.
  597.      * @throws NoResponseException
  598.      * @throws XMPPErrorException
  599.      * @throws NotConnectedException
  600.      * @throws InterruptedException
  601.      * @since 4.1
  602.      */
  603.     public boolean serverSupportsFeature(String feature) throws NoResponseException, XMPPErrorException,
  604.                     NotConnectedException, InterruptedException {
  605.         return supportsFeature(connection().getServiceName(), feature);
  606.     }

  607.     /**
  608.      * Queries the remote entity for it's features and returns true if the given feature is found.
  609.      *
  610.      * @param jid the JID of the remote entity
  611.      * @param feature
  612.      * @return true if the entity supports the feature, false otherwise
  613.      * @throws XMPPErrorException
  614.      * @throws NoResponseException
  615.      * @throws NotConnectedException
  616.      * @throws InterruptedException
  617.      */
  618.     public boolean supportsFeature(Jid jid, String feature) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  619.         DiscoverInfo result = discoverInfo(jid);
  620.         return result.containsFeature(feature);
  621.     }

  622.     /**
  623.      * Create a cache to hold the 25 most recently lookup services for a given feature for a period
  624.      * of 24 hours.
  625.      */
  626.     private Cache<String, List<DomainBareJid>> services = new ExpirationCache<>(25,
  627.                     24 * 60 * 60 * 1000);

  628.     /**
  629.      * Find all services under the users service that provide a given feature.
  630.      *
  631.      * @param feature the feature to search for
  632.      * @param stopOnFirst if true, stop searching after the first service was found
  633.      * @param useCache if true, query a cache first to avoid network I/O
  634.      * @return a possible empty list of services providing the given feature
  635.      * @throws NoResponseException
  636.      * @throws XMPPErrorException
  637.      * @throws NotConnectedException
  638.      * @throws InterruptedException
  639.      */
  640.     public List<DomainBareJid> findServices(String feature, boolean stopOnFirst, boolean useCache)
  641.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  642.         List<DomainBareJid> serviceAddresses = null;
  643.         DomainBareJid serviceName = connection().getServiceName();
  644.         if (useCache) {
  645.             serviceAddresses = services.get(feature);
  646.             if (serviceAddresses != null) {
  647.                 return serviceAddresses;
  648.             }
  649.         }
  650.         serviceAddresses = new LinkedList<>();
  651.         // Send the disco packet to the server itself
  652.         DiscoverInfo info;
  653.         try {
  654.             info = discoverInfo(serviceName);
  655.         } catch (XMPPErrorException e) {
  656.             // Be extra robust here: Return the empty linked list and log this situation
  657.             LOGGER.log(Level.WARNING, "Could not discover information about service", e);
  658.             return serviceAddresses;
  659.         }
  660.         // Check if the server supports XEP-33
  661.         if (info.containsFeature(feature)) {
  662.             serviceAddresses.add(serviceName);
  663.             if (stopOnFirst) {
  664.                 if (useCache) {
  665.                     // Cache the discovered information
  666.                     services.put(feature, serviceAddresses);
  667.                 }
  668.                 return serviceAddresses;
  669.             }
  670.         }
  671.         DiscoverItems items;
  672.         try {
  673.             // Get the disco items and send the disco packet to each server item
  674.             items = discoverItems(serviceName);
  675.         } catch(XMPPErrorException e) {
  676.             LOGGER.log(Level.WARNING, "Could not discover items about service", e);
  677.             return serviceAddresses;
  678.         }
  679.         for (DiscoverItems.Item item : items.getItems()) {
  680.             try {
  681.                 // TODO is it OK here in all cases to query without the node attribute?
  682.                 // MultipleRecipientManager queried initially also with the node attribute, but this
  683.                 // could be simply a fault instead of intentional.
  684.                 info = discoverInfo(item.getEntityID());
  685.             }
  686.             catch (XMPPErrorException | NoResponseException e) {
  687.                 // Don't throw this exceptions if one of the server's items fail
  688.                 LOGGER.log(Level.WARNING, "Exception while discovering info for feature " + feature
  689.                                 + " of " + item.getEntityID() + " node: " + item.getNode(), e);
  690.                 continue;
  691.             }
  692.             if (info.containsFeature(feature)) {
  693.                 serviceAddresses.add(item.getEntityID().asDomainBareJid());
  694.                 if (stopOnFirst) {
  695.                     break;
  696.                 }
  697.             }
  698.         }
  699.         if (useCache) {
  700.             // Cache the discovered information
  701.             services.put(feature, serviceAddresses);
  702.         }
  703.         return serviceAddresses;
  704.     }

  705.     /**
  706.      * Entity Capabilities
  707.      */

  708.     /**
  709.      * Loads the ServiceDiscoveryManager with an EntityCapsManger that speeds up certain lookups.
  710.      *
  711.      * @param manager
  712.      */
  713.     public void setEntityCapsManager(EntityCapsManager manager) {
  714.         capsManager = manager;
  715.     }

  716.     /**
  717.      * Updates the Entity Capabilities Verification String if EntityCaps is enabled.
  718.      */
  719.     private void renewEntityCapsVersion() {
  720.         if (capsManager != null && capsManager.entityCapsEnabled())
  721.             capsManager.updateLocalEntityCaps();
  722.     }
  723. }