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