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