1/**
2 * $RCSfile$
3 * $Revision$
4 * $Date$
5 *
6 * Copyright 2003-2007 Jive Software.
7 *
8 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
9 * you may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 *     http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing, software
15 * distributed under the License is distributed on an "AS IS" BASIS,
16 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 * See the License for the specific language governing permissions and
18 * limitations under the License.
19 */
20
21package org.jivesoftware.smackx;
22
23import org.jivesoftware.smack.*;
24import org.jivesoftware.smack.filter.PacketFilter;
25import org.jivesoftware.smack.filter.PacketIDFilter;
26import org.jivesoftware.smack.filter.PacketTypeFilter;
27import org.jivesoftware.smack.packet.IQ;
28import org.jivesoftware.smack.packet.Packet;
29import org.jivesoftware.smack.packet.PacketExtension;
30import org.jivesoftware.smack.packet.XMPPError;
31import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
32import org.jivesoftware.smackx.packet.DiscoverInfo;
33import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
34import org.jivesoftware.smackx.packet.DiscoverItems;
35import org.jivesoftware.smackx.packet.DataForm;
36
37import java.util.*;
38import java.util.concurrent.ConcurrentHashMap;
39
40/**
41 * Manages discovery of services in XMPP entities. This class provides:
42 * <ol>
43 * <li>A registry of supported features in this XMPP entity.
44 * <li>Automatic response when this XMPP entity is queried for information.
45 * <li>Ability to discover items and information of remote XMPP entities.
46 * <li>Ability to publish publicly available items.
47 * </ol>
48 *
49 * @author Gaston Dombiak
50 */
51public class ServiceDiscoveryManager {
52
53    private static final String DEFAULT_IDENTITY_NAME = "Smack";
54    private static final String DEFAULT_IDENTITY_CATEGORY = "client";
55    private static final String DEFAULT_IDENTITY_TYPE = "pc";
56
57    private static List<DiscoverInfo.Identity> identities = new LinkedList<DiscoverInfo.Identity>();
58
59    private EntityCapsManager capsManager;
60
61    private static Map<Connection, ServiceDiscoveryManager> instances =
62            new ConcurrentHashMap<Connection, ServiceDiscoveryManager>();
63
64    private Connection connection;
65    private final Set<String> features = new HashSet<String>();
66    private DataForm extendedInfo = null;
67    private Map<String, NodeInformationProvider> nodeInformationProviders =
68            new ConcurrentHashMap<String, NodeInformationProvider>();
69
70    // Create a new ServiceDiscoveryManager on every established connection
71    static {
72        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
73            public void connectionCreated(Connection connection) {
74                new ServiceDiscoveryManager(connection);
75            }
76        });
77        identities.add(new Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE));
78    }
79
80    /**
81     * Creates a new ServiceDiscoveryManager for a given Connection. This means that the
82     * service manager will respond to any service discovery request that the connection may
83     * receive.
84     *
85     * @param connection the connection to which a ServiceDiscoveryManager is going to be created.
86     */
87    public ServiceDiscoveryManager(Connection connection) {
88        this.connection = connection;
89
90        init();
91    }
92
93    /**
94     * Returns the ServiceDiscoveryManager instance associated with a given Connection.
95     *
96     * @param connection the connection used to look for the proper ServiceDiscoveryManager.
97     * @return the ServiceDiscoveryManager associated with a given Connection.
98     */
99    public static ServiceDiscoveryManager getInstanceFor(Connection connection) {
100        return instances.get(connection);
101    }
102
103    /**
104     * Returns the name of the client that will be returned when asked for the client identity
105     * in a disco request. The name could be any value you need to identity this client.
106     *
107     * @return the name of the client that will be returned when asked for the client identity
108     *          in a disco request.
109     */
110    public static String getIdentityName() {
111        DiscoverInfo.Identity identity = identities.get(0);
112        if (identity != null) {
113            return identity.getName();
114        } else {
115            return null;
116        }
117    }
118
119    /**
120     * Sets the name of the client that will be returned when asked for the client identity
121     * in a disco request. The name could be any value you need to identity this client.
122     *
123     * @param name the name of the client that will be returned when asked for the client identity
124     *          in a disco request.
125     */
126    public static void setIdentityName(String name) {
127        DiscoverInfo.Identity identity = identities.remove(0);
128        identity = new DiscoverInfo.Identity(DEFAULT_IDENTITY_CATEGORY, name, DEFAULT_IDENTITY_TYPE);
129        identities.add(identity);
130    }
131
132    /**
133     * Returns the type of client that will be returned when asked for the client identity in a
134     * disco request. The valid types are defined by the category client. Follow this link to learn
135     * the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>.
136     *
137     * @return the type of client that will be returned when asked for the client identity in a
138     *          disco request.
139     */
140    public static String getIdentityType() {
141        DiscoverInfo.Identity identity = identities.get(0);
142        if (identity != null) {
143            return identity.getType();
144        } else {
145            return null;
146        }
147    }
148
149    /**
150     * Sets the type of client that will be returned when asked for the client identity in a
151     * disco request. The valid types are defined by the category client. Follow this link to learn
152     * the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>.
153     *
154     * @param type the type of client that will be returned when asked for the client identity in a
155     *          disco request.
156     */
157    public static void setIdentityType(String type) {
158        DiscoverInfo.Identity identity = identities.get(0);
159        if (identity != null) {
160            identity.setType(type);
161        } else {
162            identity = new DiscoverInfo.Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, type);
163            identities.add(identity);
164        }
165    }
166
167    /**
168     * Returns all identities of this client as unmodifiable Collection
169     *
170     * @return
171     */
172    public static List<DiscoverInfo.Identity> getIdentities() {
173        return Collections.unmodifiableList(identities);
174    }
175
176    /**
177     * Initializes the packet listeners of the connection that will answer to any
178     * service discovery request.
179     */
180    private void init() {
181        // Register the new instance and associate it with the connection
182        instances.put(connection, this);
183
184        addFeature(DiscoverInfo.NAMESPACE);
185        addFeature(DiscoverItems.NAMESPACE);
186
187        // Add a listener to the connection that removes the registered instance when
188        // the connection is closed
189        connection.addConnectionListener(new ConnectionListener() {
190            public void connectionClosed() {
191                // Unregister this instance since the connection has been closed
192                instances.remove(connection);
193            }
194
195            public void connectionClosedOnError(Exception e) {
196                // ignore
197            }
198
199            public void reconnectionFailed(Exception e) {
200                // ignore
201            }
202
203            public void reconnectingIn(int seconds) {
204                // ignore
205            }
206
207            public void reconnectionSuccessful() {
208                // ignore
209            }
210        });
211
212        // Listen for disco#items requests and answer with an empty result
213        PacketFilter packetFilter = new PacketTypeFilter(DiscoverItems.class);
214        PacketListener packetListener = new PacketListener() {
215            public void processPacket(Packet packet) {
216                DiscoverItems discoverItems = (DiscoverItems) packet;
217                // Send back the items defined in the client if the request is of type GET
218                if (discoverItems != null && discoverItems.getType() == IQ.Type.GET) {
219                    DiscoverItems response = new DiscoverItems();
220                    response.setType(IQ.Type.RESULT);
221                    response.setTo(discoverItems.getFrom());
222                    response.setPacketID(discoverItems.getPacketID());
223                    response.setNode(discoverItems.getNode());
224
225                    // Add the defined items related to the requested node. Look for
226                    // the NodeInformationProvider associated with the requested node.
227                    NodeInformationProvider nodeInformationProvider =
228                            getNodeInformationProvider(discoverItems.getNode());
229                    if (nodeInformationProvider != null) {
230                        // Specified node was found, add node items
231                        response.addItems(nodeInformationProvider.getNodeItems());
232                        // Add packet extensions
233                        response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
234                    } else if(discoverItems.getNode() != null) {
235                        // Return <item-not-found/> error since client doesn't contain
236                        // the specified node
237                        response.setType(IQ.Type.ERROR);
238                        response.setError(new XMPPError(XMPPError.Condition.item_not_found));
239                    }
240                    connection.sendPacket(response);
241                }
242            }
243        };
244        connection.addPacketListener(packetListener, packetFilter);
245
246        // Listen for disco#info requests and answer the client's supported features
247        // To add a new feature as supported use the #addFeature message
248        packetFilter = new PacketTypeFilter(DiscoverInfo.class);
249        packetListener = new PacketListener() {
250            public void processPacket(Packet packet) {
251                DiscoverInfo discoverInfo = (DiscoverInfo) packet;
252                // Answer the client's supported features if the request is of the GET type
253                if (discoverInfo != null && discoverInfo.getType() == IQ.Type.GET) {
254                    DiscoverInfo response = new DiscoverInfo();
255                    response.setType(IQ.Type.RESULT);
256                    response.setTo(discoverInfo.getFrom());
257                    response.setPacketID(discoverInfo.getPacketID());
258                    response.setNode(discoverInfo.getNode());
259                    // Add the client's identity and features only if "node" is null
260                    // and if the request was not send to a node. If Entity Caps are
261                    // enabled the client's identity and features are may also added
262                    // if the right node is chosen
263                    if (discoverInfo.getNode() == null) {
264                        addDiscoverInfoTo(response);
265                    }
266                    else {
267                        // Disco#info was sent to a node. Check if we have information of the
268                        // specified node
269                        NodeInformationProvider nodeInformationProvider =
270                                getNodeInformationProvider(discoverInfo.getNode());
271                        if (nodeInformationProvider != null) {
272                            // Node was found. Add node features
273                            response.addFeatures(nodeInformationProvider.getNodeFeatures());
274                            // Add node identities
275                            response.addIdentities(nodeInformationProvider.getNodeIdentities());
276                            // Add packet extensions
277                            response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
278                        }
279                        else {
280                            // Return <item-not-found/> error since specified node was not found
281                            response.setType(IQ.Type.ERROR);
282                            response.setError(new XMPPError(XMPPError.Condition.item_not_found));
283                        }
284                    }
285                    connection.sendPacket(response);
286                }
287            }
288        };
289        connection.addPacketListener(packetListener, packetFilter);
290    }
291
292    /**
293     * Add discover info response data.
294     *
295     * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol; Example 2</a>
296     *
297     * @param response the discover info response packet
298     */
299    public void addDiscoverInfoTo(DiscoverInfo response) {
300        // First add the identities of the connection
301        response.addIdentities(identities);
302
303        // Add the registered features to the response
304        synchronized (features) {
305            for (Iterator<String> it = getFeatures(); it.hasNext();) {
306                response.addFeature(it.next());
307            }
308            response.addExtension(extendedInfo);
309        }
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     *
364     * @return an Iterator on the supported features by this XMPP entity.
365     */
366    public Iterator<String> getFeatures() {
367        synchronized (features) {
368            return Collections.unmodifiableList(new ArrayList<String>(features)).iterator();
369        }
370    }
371
372    /**
373     * Returns the supported features by this XMPP entity.
374     *
375     * @return a copy of the List on the supported features by this XMPP entity.
376     */
377    public List<String> getFeaturesList() {
378        synchronized (features) {
379            return new LinkedList<String>(features);
380        }
381    }
382
383    /**
384     * Registers that a new feature is supported by this XMPP entity. When this client is
385     * queried for its information the registered features will be answered.<p>
386     *
387     * Since no packet is actually sent to the server it is safe to perform this operation
388     * before logging to the server. In fact, you may want to configure the supported features
389     * before logging to the server so that the information is already available if it is required
390     * upon login.
391     *
392     * @param feature the feature to register as supported.
393     */
394    public void addFeature(String feature) {
395        synchronized (features) {
396            features.add(feature);
397            renewEntityCapsVersion();
398        }
399    }
400
401    /**
402     * Removes the specified feature from the supported features by this XMPP entity.<p>
403     *
404     * Since no packet is actually sent to the server it is safe to perform this operation
405     * before logging to the server.
406     *
407     * @param feature the feature to remove from the supported features.
408     */
409    public void removeFeature(String feature) {
410        synchronized (features) {
411            features.remove(feature);
412            renewEntityCapsVersion();
413        }
414    }
415
416    /**
417     * Returns true if the specified feature is registered in the ServiceDiscoveryManager.
418     *
419     * @param feature the feature to look for.
420     * @return a boolean indicating if the specified featured is registered or not.
421     */
422    public boolean includesFeature(String feature) {
423        synchronized (features) {
424            return features.contains(feature);
425        }
426    }
427
428    /**
429     * Registers extended discovery information of this XMPP entity. When this
430     * client is queried for its information this data form will be returned as
431     * specified by XEP-0128.
432     * <p>
433     *
434     * Since no packet is actually sent to the server it is safe to perform this
435     * operation before logging to the server. In fact, you may want to
436     * configure the extended info before logging to the server so that the
437     * information is already available if it is required upon login.
438     *
439     * @param info
440     *            the data form that contains the extend service discovery
441     *            information.
442     */
443    public void setExtendedInfo(DataForm info) {
444      extendedInfo = info;
445      renewEntityCapsVersion();
446    }
447
448    /**
449     * Returns the data form that is set as extended information for this Service Discovery instance (XEP-0128)
450     *
451     * @see <a href="http://xmpp.org/extensions/xep-0128.html">XEP-128: Service Discovery Extensions</a>
452     * @return
453     */
454    public DataForm getExtendedInfo() {
455        return extendedInfo;
456    }
457
458    /**
459     * Returns the data form as List of PacketExtensions, or null if no data form is set.
460     * This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider)
461     *
462     * @return
463     */
464    public List<PacketExtension> getExtendedInfoAsList() {
465        List<PacketExtension> res = null;
466        if (extendedInfo != null) {
467            res = new ArrayList<PacketExtension>(1);
468            res.add(extendedInfo);
469        }
470        return res;
471    }
472
473    /**
474     * Removes the data form containing extended service discovery information
475     * from the information returned by this XMPP entity.<p>
476     *
477     * Since no packet is actually sent to the server it is safe to perform this
478     * operation before logging to the server.
479     */
480    public void removeExtendedInfo() {
481       extendedInfo = null;
482       renewEntityCapsVersion();
483    }
484
485    /**
486     * Returns the discovered information of a given XMPP entity addressed by its JID.
487     * Use null as entityID to query the server
488     *
489     * @param entityID the address of the XMPP entity or null.
490     * @return the discovered information.
491     * @throws XMPPException if the operation failed for some reason.
492     */
493    public DiscoverInfo discoverInfo(String entityID) throws XMPPException {
494        if (entityID == null)
495            return discoverInfo(null, null);
496
497        // Check if the have it cached in the Entity Capabilities Manager
498        DiscoverInfo info = EntityCapsManager.getDiscoverInfoByUser(entityID);
499
500        if (info != null) {
501            // We were able to retrieve the information from Entity Caps and
502            // avoided a disco request, hurray!
503            return info;
504        }
505
506        // Try to get the newest node#version if it's known, otherwise null is
507        // returned
508        EntityCapsManager.NodeVerHash nvh = EntityCapsManager.getNodeVerHashByJid(entityID);
509
510        // Discover by requesting the information from the remote entity
511        // Note that wee need to use NodeVer as argument for Node if it exists
512        info = discoverInfo(entityID, nvh != null ? nvh.getNodeVer() : null);
513
514        // If the node version is known, store the new entry.
515        if (nvh != null) {
516            if (EntityCapsManager.verifyDiscoverInfoVersion(nvh.getVer(), nvh.getHash(), info))
517                EntityCapsManager.addDiscoverInfoByNode(nvh.getNodeVer(), info);
518        }
519
520        return info;
521    }
522
523    /**
524     * Returns the discovered information of a given XMPP entity addressed by its JID and
525     * note attribute. Use this message only when trying to query information which is not
526     * directly addressable.
527     *
528     * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a>
529     * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a>
530     *
531     * @param entityID the address of the XMPP entity.
532     * @param node the optional attribute that supplements the 'jid' attribute.
533     * @return the discovered information.
534     * @throws XMPPException if the operation failed for some reason.
535     */
536    public DiscoverInfo discoverInfo(String entityID, String node) throws XMPPException {
537        // Discover the entity's info
538        DiscoverInfo disco = new DiscoverInfo();
539        disco.setType(IQ.Type.GET);
540        disco.setTo(entityID);
541        disco.setNode(node);
542
543        // Create a packet collector to listen for a response.
544        PacketCollector collector =
545            connection.createPacketCollector(new PacketIDFilter(disco.getPacketID()));
546
547        connection.sendPacket(disco);
548
549        // Wait up to 5 seconds for a result.
550        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
551        // Stop queuing results
552        collector.cancel();
553        if (result == null) {
554            throw new XMPPException("No response from the server.");
555        }
556        if (result.getType() == IQ.Type.ERROR) {
557            throw new XMPPException(result.getError());
558        }
559        return (DiscoverInfo) result;
560    }
561
562    /**
563     * Returns the discovered items of a given XMPP entity addressed by its JID.
564     *
565     * @param entityID the address of the XMPP entity.
566     * @return the discovered information.
567     * @throws XMPPException if the operation failed for some reason.
568     */
569    public DiscoverItems discoverItems(String entityID) throws XMPPException {
570        return discoverItems(entityID, null);
571    }
572
573    /**
574     * Returns the discovered items of a given XMPP entity addressed by its JID and
575     * note attribute. Use this message only when trying to query information which is not
576     * directly addressable.
577     *
578     * @param entityID the address of the XMPP entity.
579     * @param node the optional attribute that supplements the 'jid' attribute.
580     * @return the discovered items.
581     * @throws XMPPException if the operation failed for some reason.
582     */
583    public DiscoverItems discoverItems(String entityID, String node) throws XMPPException {
584        // Discover the entity's items
585        DiscoverItems disco = new DiscoverItems();
586        disco.setType(IQ.Type.GET);
587        disco.setTo(entityID);
588        disco.setNode(node);
589
590        // Create a packet collector to listen for a response.
591        PacketCollector collector =
592            connection.createPacketCollector(new PacketIDFilter(disco.getPacketID()));
593
594        connection.sendPacket(disco);
595
596        // Wait up to 5 seconds for a result.
597        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
598        // Stop queuing results
599        collector.cancel();
600        if (result == null) {
601            throw new XMPPException("No response from the server.");
602        }
603        if (result.getType() == IQ.Type.ERROR) {
604            throw new XMPPException(result.getError());
605        }
606        return (DiscoverItems) result;
607    }
608
609    /**
610     * Returns true if the server supports publishing of items. A client may wish to publish items
611     * to the server so that the server can provide items associated to the client. These items will
612     * be returned by the server whenever the server receives a disco request targeted to the bare
613     * address of the client (i.e. user@host.com).
614     *
615     * @param entityID the address of the XMPP entity.
616     * @return true if the server supports publishing of items.
617     * @throws XMPPException if the operation failed for some reason.
618     */
619    public boolean canPublishItems(String entityID) throws XMPPException {
620        DiscoverInfo info = discoverInfo(entityID);
621        return canPublishItems(info);
622     }
623
624     /**
625      * Returns true if the server supports publishing of items. A client may wish to publish items
626      * to the server so that the server can provide items associated to the client. These items will
627      * be returned by the server whenever the server receives a disco request targeted to the bare
628      * address of the client (i.e. user@host.com).
629      *
630      * @param DiscoverInfo the discover info packet to check.
631      * @return true if the server supports publishing of items.
632      */
633     public static boolean canPublishItems(DiscoverInfo info) {
634         return info.containsFeature("http://jabber.org/protocol/disco#publish");
635     }
636
637    /**
638     * Publishes new items to a parent entity. The item elements to publish MUST have at least
639     * a 'jid' attribute specifying the Entity ID of the item, and an action attribute which
640     * specifies the action being taken for that item. Possible action values are: "update" and
641     * "remove".
642     *
643     * @param entityID the address of the XMPP entity.
644     * @param discoverItems the DiscoveryItems to publish.
645     * @throws XMPPException if the operation failed for some reason.
646     */
647    public void publishItems(String entityID, DiscoverItems discoverItems)
648            throws XMPPException {
649        publishItems(entityID, null, discoverItems);
650    }
651
652    /**
653     * Publishes new items to a parent entity and node. The item elements to publish MUST have at
654     * least a 'jid' attribute specifying the Entity ID of the item, and an action attribute which
655     * specifies the action being taken for that item. Possible action values are: "update" and
656     * "remove".
657     *
658     * @param entityID the address of the XMPP entity.
659     * @param node the attribute that supplements the 'jid' attribute.
660     * @param discoverItems the DiscoveryItems to publish.
661     * @throws XMPPException if the operation failed for some reason.
662     */
663    public void publishItems(String entityID, String node, DiscoverItems discoverItems)
664            throws XMPPException {
665        discoverItems.setType(IQ.Type.SET);
666        discoverItems.setTo(entityID);
667        discoverItems.setNode(node);
668
669        // Create a packet collector to listen for a response.
670        PacketCollector collector =
671            connection.createPacketCollector(new PacketIDFilter(discoverItems.getPacketID()));
672
673        connection.sendPacket(discoverItems);
674
675        // Wait up to 5 seconds for a result.
676        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
677        // Stop queuing results
678        collector.cancel();
679        if (result == null) {
680            throw new XMPPException("No response from the server.");
681        }
682        if (result.getType() == IQ.Type.ERROR) {
683            throw new XMPPException(result.getError());
684        }
685    }
686
687    /**
688     * Entity Capabilities
689     */
690
691    /**
692     * Loads the ServiceDiscoveryManager with an EntityCapsManger
693     * that speeds up certain lookups
694     * @param manager
695     */
696    public void setEntityCapsManager(EntityCapsManager manager) {
697        capsManager = manager;
698    }
699
700    /**
701     * Updates the Entity Capabilities Verification String
702     * if EntityCaps is enabled
703     */
704    private void renewEntityCapsVersion() {
705        if (capsManager != null && capsManager.entityCapsEnabled())
706            capsManager.updateLocalEntityCaps();
707    }
708}
709