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