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