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