• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

igniterealtime / Smack / #2888

26 Oct 2023 07:08PM UTC coverage: 39.106%. Remained the same
#2888

push

github-actions

web-flow
Merge pull request #568 from guusdk/intellij-icon

Add icon to IntelliJ metadata

16364 of 41845 relevant lines covered (39.11%)

0.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

43.17
/smack-extensions/src/main/java/org/jivesoftware/smackx/disco/ServiceDiscoveryManager.java
1
/**
2
 *
3
 * Copyright 2003-2007 Jive Software, 2018-2022 Florian Schmaus.
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *     http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
package org.jivesoftware.smackx.disco;
18

19
import java.io.IOException;
20
import java.util.ArrayList;
21
import java.util.Arrays;
22
import java.util.Collection;
23
import java.util.Collections;
24
import java.util.HashSet;
25
import java.util.LinkedList;
26
import java.util.List;
27
import java.util.Map;
28
import java.util.Set;
29
import java.util.WeakHashMap;
30
import java.util.concurrent.ConcurrentHashMap;
31
import java.util.concurrent.CopyOnWriteArraySet;
32
import java.util.concurrent.TimeUnit;
33
import java.util.concurrent.atomic.AtomicInteger;
34
import java.util.logging.Level;
35
import java.util.logging.Logger;
36

37
import org.jivesoftware.smack.ConnectionCreationListener;
38
import org.jivesoftware.smack.ConnectionListener;
39
import org.jivesoftware.smack.Manager;
40
import org.jivesoftware.smack.ScheduledAction;
41
import org.jivesoftware.smack.SmackException.NoResponseException;
42
import org.jivesoftware.smack.SmackException.NotConnectedException;
43
import org.jivesoftware.smack.XMPPConnection;
44
import org.jivesoftware.smack.XMPPConnectionRegistry;
45
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
46
import org.jivesoftware.smack.filter.PresenceTypeFilter;
47
import org.jivesoftware.smack.internal.AbstractStats;
48
import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler;
49
import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode;
50
import org.jivesoftware.smack.packet.IQ;
51
import org.jivesoftware.smack.packet.Presence;
52
import org.jivesoftware.smack.packet.Stanza;
53
import org.jivesoftware.smack.packet.StanzaError;
54
import org.jivesoftware.smack.util.CollectionUtil;
55
import org.jivesoftware.smack.util.ExtendedAppendable;
56
import org.jivesoftware.smack.util.Objects;
57
import org.jivesoftware.smack.util.StringUtils;
58

59
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
60
import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity;
61
import org.jivesoftware.smackx.disco.packet.DiscoverInfoBuilder;
62
import org.jivesoftware.smackx.disco.packet.DiscoverItems;
63
import org.jivesoftware.smackx.xdata.packet.DataForm;
64

65
import org.jxmpp.jid.DomainBareJid;
66
import org.jxmpp.jid.EntityBareJid;
67
import org.jxmpp.jid.Jid;
68
import org.jxmpp.util.cache.Cache;
69
import org.jxmpp.util.cache.ExpirationCache;
70

71
/**
72
 * Manages discovery of services in XMPP entities. This class provides:
73
 * <ol>
74
 * <li>A registry of supported features in this XMPP entity.
75
 * <li>Automatic response when this XMPP entity is queried for information.
76
 * <li>Ability to discover items and information of remote XMPP entities.
77
 * <li>Ability to publish publicly available items.
78
 * </ol>
79
 *
80
 * @author Gaston Dombiak
81
 * @author Florian Schmaus
82
 */
83
public final class ServiceDiscoveryManager extends Manager {
84

85
    private static final Logger LOGGER = Logger.getLogger(ServiceDiscoveryManager.class.getName());
1✔
86

87
    private static final String DEFAULT_IDENTITY_NAME = "Smack";
88
    private static final String DEFAULT_IDENTITY_CATEGORY = "client";
89
    private static final String DEFAULT_IDENTITY_TYPE = "pc";
90

91
    private static final List<DiscoInfoLookupShortcutMechanism> discoInfoLookupShortcutMechanisms = new ArrayList<>(2);
1✔
92

93
    private static DiscoverInfo.Identity defaultIdentity = new Identity(DEFAULT_IDENTITY_CATEGORY,
1✔
94
            DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE);
95

96
    private final Set<DiscoverInfo.Identity> identities = new HashSet<>();
1✔
97
    private DiscoverInfo.Identity identity = defaultIdentity;
1✔
98

99
    private final Set<EntityCapabilitiesChangedListener> entityCapabilitiesChangedListeners = new CopyOnWriteArraySet<>();
1✔
100

101
    private static final Map<XMPPConnection, ServiceDiscoveryManager> instances = new WeakHashMap<>();
1✔
102

103
    private final Set<String> features = new HashSet<>();
1✔
104
    private List<DataForm> extendedInfos = new ArrayList<>(2);
1✔
105
    private final Map<String, NodeInformationProvider> nodeInformationProviders = new ConcurrentHashMap<>();
1✔
106

107
    private volatile Presence presenceSend;
108

109
    // Create a new ServiceDiscoveryManager on every established connection
110
    static {
111
        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
1✔
112
            @Override
113
            public void connectionCreated(XMPPConnection connection) {
114
                getInstanceFor(connection);
1✔
115
            }
1✔
116
        });
117
    }
1✔
118

119
    /**
120
     * Set the default identity all new connections will have. If unchanged the default identity is an
121
     * identity where category is set to 'client', type is set to 'pc' and name is set to 'Smack'.
122
     *
123
     * @param identity TODO javadoc me please
124
     */
125
    public static void setDefaultIdentity(DiscoverInfo.Identity identity) {
126
        defaultIdentity = identity;
×
127
    }
×
128

129
    /**
130
     * Creates a new ServiceDiscoveryManager for a given XMPPConnection. This means that the
131
     * service manager will respond to any service discovery request that the connection may
132
     * receive.
133
     *
134
     * @param connection the connection to which a ServiceDiscoveryManager is going to be created.
135
     */
136
    private ServiceDiscoveryManager(XMPPConnection connection) {
137
        super(connection);
1✔
138

139
        addFeature(DiscoverInfo.NAMESPACE);
1✔
140
        addFeature(DiscoverItems.NAMESPACE);
1✔
141

142
        // Listen for disco#items requests and answer with an empty result
143
        connection.registerIQRequestHandler(new AbstractIqRequestHandler(DiscoverItems.ELEMENT, DiscoverItems.NAMESPACE, IQ.Type.get, Mode.async) {
1✔
144
            @Override
145
            public IQ handleIQRequest(IQ iqRequest) {
146
                DiscoverItems discoverItems = (DiscoverItems) iqRequest;
×
147
                DiscoverItems response = new DiscoverItems();
×
148
                response.setType(IQ.Type.result);
×
149
                response.setTo(discoverItems.getFrom());
×
150
                response.setStanzaId(discoverItems.getStanzaId());
×
151
                response.setNode(discoverItems.getNode());
×
152

153
                // Add the defined items related to the requested node. Look for
154
                // the NodeInformationProvider associated with the requested node.
155
                NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverItems.getNode());
×
156
                if (nodeInformationProvider != null) {
×
157
                    // Specified node was found, add node items
158
                    response.addItems(nodeInformationProvider.getNodeItems());
×
159
                    // Add packet extensions
160
                    response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
×
161
                } else if (discoverItems.getNode() != null) {
×
162
                    // Return <item-not-found/> error since client doesn't contain
163
                    // the specified node
164
                    response.setType(IQ.Type.error);
×
165
                    response.setError(StanzaError.getBuilder(StanzaError.Condition.item_not_found).build());
×
166
                }
167
                return response;
×
168
            }
169
        });
170

171
        // Listen for disco#info requests and answer the client's supported features
172
        // To add a new feature as supported use the #addFeature message
173
        connection.registerIQRequestHandler(new AbstractIqRequestHandler(DiscoverInfo.ELEMENT, DiscoverInfo.NAMESPACE, IQ.Type.get, Mode.async) {
1✔
174
            @Override
175
            public IQ handleIQRequest(IQ iqRequest) {
176
                DiscoverInfo discoverInfo = (DiscoverInfo) iqRequest;
×
177
                // Answer the client's supported features if the request is of the GET type
178
                DiscoverInfoBuilder responseBuilder = DiscoverInfoBuilder.buildResponseFor(discoverInfo, IQ.ResponseType.result);
×
179

180
                // Add the client's identity and features only if "node" is null
181
                // and if the request was not send to a node. If Entity Caps are
182
                // enabled the client's identity and features are may also added
183
                // if the right node is chosen
184
                if (discoverInfo.getNode() == null) {
×
185
                    addDiscoverInfoTo(responseBuilder);
×
186
                } else {
187
                    // Disco#info was sent to a node. Check if we have information of the
188
                    // specified node
189
                    NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverInfo.getNode());
×
190
                    if (nodeInformationProvider != null) {
×
191
                        // Node was found. Add node features
192
                        responseBuilder.addFeatures(nodeInformationProvider.getNodeFeatures());
×
193
                        // Add node identities
194
                        responseBuilder.addIdentities(nodeInformationProvider.getNodeIdentities());
×
195
                        // Add packet extensions
196
                        responseBuilder.addOptExtensions(nodeInformationProvider.getNodePacketExtensions());
×
197
                    } else {
198
                        // Return <item-not-found/> error since specified node was not found
199
                        responseBuilder.ofType(IQ.Type.error);
×
200
                        responseBuilder.setError(StanzaError.getBuilder(StanzaError.Condition.item_not_found).build());
×
201
                    }
202
                }
203

204
                DiscoverInfo response = responseBuilder.build();
×
205
                return response;
×
206
            }
207
        });
208

209
        connection.addConnectionListener(new ConnectionListener() {
1✔
210
            @Override
211
            public void authenticated(XMPPConnection connection, boolean resumed) {
212
                // Reset presenceSend when the connection was not resumed
213
                if (!resumed) {
×
214
                    presenceSend = null;
×
215
                }
216
            }
×
217
        });
218
        connection.addStanzaSendingListener(p -> presenceSend = (Presence) p,
1✔
219
                        PresenceTypeFilter.OUTGOING_PRESENCE_BROADCAST);
220
    }
1✔
221

222
    /**
223
     * Returns the name of the client that will be returned when asked for the client identity
224
     * in a disco request. The name could be any value you need to identity this client.
225
     *
226
     * @return the name of the client that will be returned when asked for the client identity
227
     *          in a disco request.
228
     */
229
    public String getIdentityName() {
230
        return identity.getName();
×
231
    }
232

233
    /**
234
     * Sets the default identity the client will report.
235
     *
236
     * @param identity TODO javadoc me please
237
     */
238
    public synchronized void setIdentity(Identity identity) {
239
        this.identity = Objects.requireNonNull(identity, "Identity can not be null");
×
240
        // Notify others of a state change of SDM. In order to keep the state consistent, this
241
        // method is synchronized
242
        renewEntityCapsVersion();
×
243
    }
×
244

245
    /**
246
     * Return the default identity of the client.
247
     *
248
     * @return the default identity.
249
     */
250
    public Identity getIdentity() {
251
        return identity;
×
252
    }
253

254
    /**
255
     * Returns the type of client that will be returned when asked for the client identity in a
256
     * disco request. The valid types are defined by the category client. Follow this link to learn
257
     * the possible types: <a href="https://xmpp.org/registrar/disco-categories.html">XMPP Registry for Service Discovery Identities</a>
258
     *
259
     * @return the type of client that will be returned when asked for the client identity in a
260
     *          disco request.
261
     */
262
    public String getIdentityType() {
263
        return identity.getType();
×
264
    }
265

266
    /**
267
     * Add an further identity to the client.
268
     *
269
     * @param identity TODO javadoc me please
270
     */
271
    public synchronized void addIdentity(DiscoverInfo.Identity identity) {
272
        identities.add(identity);
×
273
        // Notify others of a state change of SDM. In order to keep the state consistent, this
274
        // method is synchronized
275
        renewEntityCapsVersion();
×
276
    }
×
277

278
    /**
279
     * Remove an identity from the client. Note that the client needs at least one identity, the default identity, which
280
     * can not be removed.
281
     *
282
     * @param identity TODO javadoc me please
283
     * @return true, if successful. Otherwise the default identity was given.
284
     */
285
    public synchronized boolean removeIdentity(DiscoverInfo.Identity identity) {
286
        if (identity.equals(this.identity)) return false;
×
287
        identities.remove(identity);
×
288
        // Notify others of a state change of SDM. In order to keep the state consistent, this
289
        // method is synchronized
290
        renewEntityCapsVersion();
×
291
        return true;
×
292
    }
293

294
    /**
295
     * Returns all identities of this client as unmodifiable Collection.
296
     *
297
     * @return all identies as set
298
     */
299
    public Set<DiscoverInfo.Identity> getIdentities() {
300
        Set<Identity> res = new HashSet<>(identities);
1✔
301
        // Add the main identity that must exist
302
        res.add(identity);
1✔
303
        return Collections.unmodifiableSet(res);
1✔
304
    }
305

306
    /**
307
     * Returns the ServiceDiscoveryManager instance associated with a given XMPPConnection.
308
     *
309
     * @param connection the connection used to look for the proper ServiceDiscoveryManager.
310
     * @return the ServiceDiscoveryManager associated with a given XMPPConnection.
311
     */
312
    public static synchronized ServiceDiscoveryManager getInstanceFor(XMPPConnection connection) {
313
        ServiceDiscoveryManager sdm = instances.get(connection);
1✔
314
        if (sdm == null) {
1✔
315
            sdm = new ServiceDiscoveryManager(connection);
1✔
316
            // Register the new instance and associate it with the connection
317
            instances.put(connection, sdm);
1✔
318
        }
319
        return sdm;
1✔
320
    }
321

322
    /**
323
     * Add discover info response data.
324
     *
325
     * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol; Example 2</a>
326
     *
327
     * @param response the discover info response packet
328
     */
329
    public synchronized void addDiscoverInfoTo(DiscoverInfoBuilder response) {
330
        // First add the identities of the connection
331
        response.addIdentities(getIdentities());
1✔
332

333
        // Add the registered features to the response
334
        for (String feature : getFeatures()) {
1✔
335
            response.addFeature(feature);
1✔
336
        }
1✔
337

338
        response.addExtensions(extendedInfos);
1✔
339
    }
1✔
340

341
    /**
342
     * Returns the NodeInformationProvider responsible for providing information
343
     * (ie items) related to a given node or <code>null</null> if none.<p>
344
     *
345
     * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
346
     * NodeInformationProvider will provide information about the rooms where the user has joined.
347
     *
348
     * @param node the node that contains items associated with an entity not addressable as a JID.
349
     * @return the NodeInformationProvider responsible for providing information related
350
     * to a given node.
351
     */
352
    private NodeInformationProvider getNodeInformationProvider(String node) {
353
        if (node == null) {
×
354
            return null;
×
355
        }
356
        return nodeInformationProviders.get(node);
×
357
    }
358

359
    /**
360
     * Sets the NodeInformationProvider responsible for providing information
361
     * (ie items) related to a given node. Every time this client receives a disco request
362
     * regarding the items of a given node, the provider associated to that node will be the
363
     * responsible for providing the requested information.<p>
364
     *
365
     * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
366
     * NodeInformationProvider will provide information about the rooms where the user has joined.
367
     *
368
     * @param node the node whose items will be provided by the NodeInformationProvider.
369
     * @param listener the NodeInformationProvider responsible for providing items related
370
     *      to the node.
371
     */
372
    public void setNodeInformationProvider(String node, NodeInformationProvider listener) {
373
        nodeInformationProviders.put(node, listener);
1✔
374
    }
1✔
375

376
    /**
377
     * Removes the NodeInformationProvider responsible for providing information
378
     * (ie items) related to a given node. This means that no more information will be
379
     * available for the specified node.
380
     *
381
     * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
382
     * NodeInformationProvider will provide information about the rooms where the user has joined.
383
     *
384
     * @param node the node to remove the associated NodeInformationProvider.
385
     */
386
    public void removeNodeInformationProvider(String node) {
387
        nodeInformationProviders.remove(node);
×
388
    }
×
389

390
    /**
391
     * Returns the supported features by this XMPP entity.
392
     * <p>
393
     * The result is a copied modifiable list of the original features.
394
     * </p>
395
     *
396
     * @return a List of the supported features by this XMPP entity.
397
     */
398
    public synchronized List<String> getFeatures() {
399
        return new ArrayList<>(features);
1✔
400
    }
401

402
    /**
403
     * Registers that a new feature is supported by this XMPP entity. When this client is
404
     * queried for its information the registered features will be answered.<p>
405
     *
406
     * Since no stanza is actually sent to the server it is safe to perform this operation
407
     * before logging to the server. In fact, you may want to configure the supported features
408
     * before logging to the server so that the information is already available if it is required
409
     * upon login.
410
     *
411
     * @param feature the feature to register as supported.
412
     */
413
    public synchronized void addFeature(String feature) {
414
        features.add(feature);
1✔
415
        // Notify others of a state change of SDM. In order to keep the state consistent, this
416
        // method is synchronized
417
        renewEntityCapsVersion();
1✔
418
    }
1✔
419

420
    /**
421
     * Removes the specified feature from the supported features by this XMPP entity.<p>
422
     *
423
     * Since no stanza is actually sent to the server it is safe to perform this operation
424
     * before logging to the server.
425
     *
426
     * @param feature the feature to remove from the supported features.
427
     */
428
    public synchronized void removeFeature(String feature) {
429
        features.remove(feature);
1✔
430
        // Notify others of a state change of SDM. In order to keep the state consistent, this
431
        // method is synchronized
432
        renewEntityCapsVersion();
1✔
433
    }
1✔
434

435
    /**
436
     * Returns true if the specified feature is registered in the ServiceDiscoveryManager.
437
     *
438
     * @param feature the feature to look for.
439
     * @return a boolean indicating if the specified featured is registered or not.
440
     */
441
    public synchronized boolean includesFeature(String feature) {
442
        return features.contains(feature);
1✔
443
    }
444

445
    /**
446
     * Registers extended discovery information of this XMPP entity. When this
447
     * client is queried for its information this data form will be returned as
448
     * specified by XEP-0128.
449
     * <p>
450
     *
451
     * Since no stanza is actually sent to the server it is safe to perform this
452
     * operation before logging to the server. In fact, you may want to
453
     * configure the extended info before logging to the server so that the
454
     * information is already available if it is required upon login.
455
     *
456
     * @param info the data form that contains the extend service discovery
457
     *            information.
458
     * @deprecated use {@link #addExtendedInfo(DataForm)} instead.
459
     */
460
    // TODO: Remove in Smack 4.5
461
    @Deprecated
462
    public synchronized void setExtendedInfo(DataForm info) {
463
        addExtendedInfo(info);
×
464
    }
×
465

466
    /**
467
     * Registers extended discovery information of this XMPP entity. When this
468
     * client is queried for its information this data form will be returned as
469
     * specified by XEP-0128.
470
     * <p>
471
     *
472
     * Since no stanza is actually sent to the server it is safe to perform this
473
     * operation before logging to the server. In fact, you may want to
474
     * configure the extended info before logging to the server so that the
475
     * information is already available if it is required upon login.
476
     *
477
     * @param extendedInfo the data form that contains the extend service discovery information.
478
     * @return the old data form which got replaced (if any)
479
     * @since 4.4.0
480
     */
481
    public DataForm addExtendedInfo(DataForm extendedInfo) {
482
        String formType = extendedInfo.getFormType();
1✔
483
        StringUtils.requireNotNullNorEmpty(formType, "The data form must have a form type set");
1✔
484

485
        DataForm removedDataForm;
486
        synchronized (this) {
1✔
487
            removedDataForm = DataForm.remove(extendedInfos, formType);
1✔
488

489
            extendedInfos.add(extendedInfo);
1✔
490

491
            // Notify others of a state change of SDM. In order to keep the state consistent, this
492
            // method is synchronized
493
            renewEntityCapsVersion();
1✔
494
        }
1✔
495
        return removedDataForm;
1✔
496
    }
497

498
    /**
499
     * Remove the extended discovery information of the given form type.
500
     *
501
     * @param formType the type of the data form with the extended discovery information to remove.
502
     * @since 4.4.0
503
     */
504
    public synchronized void removeExtendedInfo(String formType) {
505
        DataForm removedForm = DataForm.remove(extendedInfos, formType);
×
506
        if (removedForm != null) {
×
507
            renewEntityCapsVersion();
×
508
        }
509
    }
×
510

511
    /**
512
     * Returns the data form as List of PacketExtensions, or null if no data form is set.
513
     * This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider)
514
     *
515
     * @return the data form as List of PacketExtensions
516
     */
517
    public synchronized List<DataForm> getExtendedInfo() {
518
        return CollectionUtil.newListWith(extendedInfos);
1✔
519
    }
520

521
    /**
522
     * Returns the data form as List of PacketExtensions, or null if no data form is set.
523
     * This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider)
524
     *
525
     * @return the data form as List of PacketExtensions
526
     * @deprecated use {@link #getExtendedInfo()} instead.
527
     */
528
    // TODO: Remove in Smack 4.5
529
    @Deprecated
530
    public List<DataForm> getExtendedInfoAsList() {
531
        return getExtendedInfo();
×
532
    }
533

534
    /**
535
     * Removes the data form containing extended service discovery information
536
     * from the information returned by this XMPP entity.<p>
537
     *
538
     * Since no stanza is actually sent to the server it is safe to perform this
539
     * operation before logging to the server.
540
     */
541
    public synchronized void removeExtendedInfo() {
542
        int extendedInfosCount = extendedInfos.size();
×
543
        extendedInfos.clear();
×
544
        if (extendedInfosCount > 0) {
×
545
            // Notify others of a state change of SDM. In order to keep the state consistent, this
546
            // method is synchronized
547
            renewEntityCapsVersion();
×
548
        }
549
    }
×
550

551
    /**
552
     * Returns the discovered information of a given XMPP entity addressed by its JID.
553
     * Use null as entityID to query the server
554
     *
555
     * @param entityID the address of the XMPP entity or null.
556
     * @return the discovered information.
557
     * @throws XMPPErrorException if there was an XMPP error returned.
558
     * @throws NoResponseException if there was no response from the remote entity.
559
     * @throws NotConnectedException if the XMPP connection is not connected.
560
     * @throws InterruptedException if the calling thread was interrupted.
561
     */
562
    public DiscoverInfo discoverInfo(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
563
        if (entityID == null)
1✔
564
            return discoverInfo(null, null);
×
565

566
        synchronized (discoInfoLookupShortcutMechanisms) {
1✔
567
            for (DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism : discoInfoLookupShortcutMechanisms) {
1✔
568
                DiscoverInfo info = discoInfoLookupShortcutMechanism.getDiscoverInfoByUser(this, entityID);
1✔
569
                if (info != null) {
1✔
570
                    // We were able to retrieve the information from Entity Caps and
571
                    // avoided a disco request, hurray!
572
                    return info;
×
573
                }
574
            }
1✔
575
        }
1✔
576

577
        // Last resort: Standard discovery.
578
        return discoverInfo(entityID, null);
1✔
579
    }
580

581
    /**
582
     * Returns the discovered information of a given XMPP entity addressed by its JID and
583
     * note attribute. Use this message only when trying to query information which is not
584
     * directly addressable.
585
     *
586
     * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a>
587
     * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a>
588
     *
589
     * @param entityID the address of the XMPP entity.
590
     * @param node the optional attribute that supplements the 'jid' attribute.
591
     * @return the discovered information.
592
     * @throws XMPPErrorException if the operation failed for some reason.
593
     * @throws NoResponseException if there was no response from the server.
594
     * @throws NotConnectedException if the XMPP connection is not connected.
595
     * @throws InterruptedException if the calling thread was interrupted.
596
     */
597
    public DiscoverInfo discoverInfo(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
598
        XMPPConnection connection = connection();
1✔
599

600
        // Discover the entity's info
601
        DiscoverInfo discoInfoRequest = DiscoverInfo.builder(connection)
1✔
602
                .to(entityID)
1✔
603
                .setNode(node)
1✔
604
                .build();
1✔
605

606
        Stanza result = connection.sendIqRequestAndWaitForResponse(discoInfoRequest);
1✔
607

608
        return (DiscoverInfo) result;
1✔
609
    }
610

611
    /**
612
     * Returns the discovered items of a given XMPP entity addressed by its JID.
613
     *
614
     * @param entityID the address of the XMPP entity.
615
     * @return the discovered information.
616
     * @throws XMPPErrorException if the operation failed for some reason.
617
     * @throws NoResponseException if there was no response from the server.
618
     * @throws NotConnectedException if the XMPP connection is not connected.
619
     * @throws InterruptedException if the calling thread was interrupted.
620
     */
621
    public DiscoverItems discoverItems(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
622
        return discoverItems(entityID, null);
1✔
623
    }
624

625
    /**
626
     * Returns the discovered items of a given XMPP entity addressed by its JID and
627
     * note attribute. Use this message only when trying to query information which is not
628
     * directly addressable.
629
     *
630
     * @param entityID the address of the XMPP entity.
631
     * @param node the optional attribute that supplements the 'jid' attribute.
632
     * @return the discovered items.
633
     * @throws XMPPErrorException if the operation failed for some reason.
634
     * @throws NoResponseException if there was no response from the server.
635
     * @throws NotConnectedException if the XMPP connection is not connected.
636
     * @throws InterruptedException if the calling thread was interrupted.
637
     */
638
    public DiscoverItems discoverItems(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
639
        // Discover the entity's items
640
        DiscoverItems disco = new DiscoverItems();
1✔
641
        disco.setType(IQ.Type.get);
1✔
642
        disco.setTo(entityID);
1✔
643
        disco.setNode(node);
1✔
644

645
        Stanza result = connection().sendIqRequestAndWaitForResponse(disco);
1✔
646
        return (DiscoverItems) result;
1✔
647
    }
648

649
    /**
650
     * Returns true if the server supports the given feature.
651
     *
652
     * @param feature TODO javadoc me please
653
     * @return true if the server supports the given feature.
654
     * @throws NoResponseException if there was no response from the remote entity.
655
     * @throws XMPPErrorException if there was an XMPP error returned.
656
     * @throws NotConnectedException if the XMPP connection is not connected.
657
     * @throws InterruptedException if the calling thread was interrupted.
658
     * @since 4.1
659
     */
660
    public boolean serverSupportsFeature(CharSequence feature) throws NoResponseException, XMPPErrorException,
661
                    NotConnectedException, InterruptedException {
662
        return serverSupportsFeatures(feature);
×
663
    }
664

665
    public boolean serverSupportsFeatures(CharSequence... features) throws NoResponseException,
666
                    XMPPErrorException, NotConnectedException, InterruptedException {
667
        return serverSupportsFeatures(Arrays.asList(features));
×
668
    }
669

670
    public boolean serverSupportsFeatures(Collection<? extends CharSequence> features)
671
                    throws NoResponseException, XMPPErrorException, NotConnectedException,
672
                    InterruptedException {
673
        return supportsFeatures(connection().getXMPPServiceDomain(), features);
×
674
    }
675

676
    /**
677
     * Check if the given features are supported by the connection account. This means that the discovery information
678
     * lookup will be performed on the bare JID of the connection managed by this ServiceDiscoveryManager.
679
     *
680
     * @param features the features to check
681
     * @return <code>true</code> if all features are supported by the connection account, <code>false</code> otherwise
682
     * @throws NoResponseException if there was no response from the remote entity.
683
     * @throws XMPPErrorException if there was an XMPP error returned.
684
     * @throws NotConnectedException if the XMPP connection is not connected.
685
     * @throws InterruptedException if the calling thread was interrupted.
686
     * @since 4.2.2
687
     */
688
    public boolean accountSupportsFeatures(CharSequence... features)
689
                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
690
        return accountSupportsFeatures(Arrays.asList(features));
×
691
    }
692

693
    /**
694
     * Check if the given collection of features are supported by the connection account. This means that the discovery
695
     * information lookup will be performed on the bare JID of the connection managed by this ServiceDiscoveryManager.
696
     *
697
     * @param features a collection of features
698
     * @return <code>true</code> if all features are supported by the connection account, <code>false</code> otherwise
699
     * @throws NoResponseException if there was no response from the remote entity.
700
     * @throws XMPPErrorException if there was an XMPP error returned.
701
     * @throws NotConnectedException if the XMPP connection is not connected.
702
     * @throws InterruptedException if the calling thread was interrupted.
703
     * @since 4.2.2
704
     */
705
    public boolean accountSupportsFeatures(Collection<? extends CharSequence> features)
706
                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
707
        EntityBareJid accountJid = connection().getUser().asEntityBareJid();
×
708
        return supportsFeatures(accountJid, features);
×
709
    }
710

711
    /**
712
     * Queries the remote entity for it's features and returns true if the given feature is found.
713
     *
714
     * @param jid the JID of the remote entity
715
     * @param feature TODO javadoc me please
716
     * @return true if the entity supports the feature, false otherwise
717
     * @throws XMPPErrorException if there was an XMPP error returned.
718
     * @throws NoResponseException if there was no response from the remote entity.
719
     * @throws NotConnectedException if the XMPP connection is not connected.
720
     * @throws InterruptedException if the calling thread was interrupted.
721
     */
722
    public boolean supportsFeature(Jid jid, CharSequence feature) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
723
        return supportsFeatures(jid, feature);
1✔
724
    }
725

726
    public boolean supportsFeatures(Jid jid, CharSequence... features) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
727
        return supportsFeatures(jid, Arrays.asList(features));
1✔
728
    }
729

730
    public boolean supportsFeatures(Jid jid, Collection<? extends CharSequence> features) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
731
        DiscoverInfo result = discoverInfo(jid);
1✔
732
        for (CharSequence feature : features) {
1✔
733
            if (!result.containsFeature(feature)) {
1✔
734
                return false;
1✔
735
            }
736
        }
1✔
737
        return true;
1✔
738
    }
739

740
    /**
741
     * Create a cache to hold the 25 most recently lookup services for a given feature for a period
742
     * of 24 hours.
743
     */
744
    private final Cache<String, List<DiscoverInfo>> services = new ExpirationCache<>(25,
1✔
745
                    24 * 60 * 60 * 1000);
746

747
    /**
748
     * Find all services under the users service that provide a given feature.
749
     *
750
     * @param feature the feature to search for
751
     * @param stopOnFirst if true, stop searching after the first service was found
752
     * @param useCache if true, query a cache first to avoid network I/O
753
     * @return a possible empty list of services providing the given feature
754
     * @throws NoResponseException if there was no response from the remote entity.
755
     * @throws XMPPErrorException if there was an XMPP error returned.
756
     * @throws NotConnectedException if the XMPP connection is not connected.
757
     * @throws InterruptedException if the calling thread was interrupted.
758
     */
759
    public List<DiscoverInfo> findServicesDiscoverInfo(String feature, boolean stopOnFirst, boolean useCache)
760
                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
761
        return findServicesDiscoverInfo(feature, stopOnFirst, useCache, null);
×
762
    }
763

764
    /**
765
     * Find all services under the users service that provide a given feature.
766
     *
767
     * @param feature the feature to search for
768
     * @param stopOnFirst if true, stop searching after the first service was found
769
     * @param useCache if true, query a cache first to avoid network I/O
770
     * @param encounteredExceptions an optional map which will be filled with the exceptions encountered
771
     * @return a possible empty list of services providing the given feature
772
     * @throws NoResponseException if there was no response from the remote entity.
773
     * @throws XMPPErrorException if there was an XMPP error returned.
774
     * @throws NotConnectedException if the XMPP connection is not connected.
775
     * @throws InterruptedException if the calling thread was interrupted.
776
     * @since 4.2.2
777
     */
778
    public List<DiscoverInfo> findServicesDiscoverInfo(String feature, boolean stopOnFirst, boolean useCache, Map<? super Jid, Exception> encounteredExceptions)
779
                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
780
        DomainBareJid serviceName = connection().getXMPPServiceDomain();
×
781
        return findServicesDiscoverInfo(serviceName, feature, stopOnFirst, useCache, encounteredExceptions);
×
782
    }
783

784
    /**
785
     * Find all services under a given service that provide a given feature.
786
     *
787
     * @param serviceName the service to query
788
     * @param feature the feature to search for
789
     * @param stopOnFirst if true, stop searching after the first service was found
790
     * @param useCache if true, query a cache first to avoid network I/O
791
     * @param encounteredExceptions an optional map which will be filled with the exceptions encountered
792
     * @return a possible empty list of services providing the given feature
793
     * @throws NoResponseException if there was no response from the remote entity.
794
     * @throws XMPPErrorException if there was an XMPP error returned.
795
     * @throws NotConnectedException if the XMPP connection is not connected.
796
     * @throws InterruptedException if the calling thread was interrupted.
797
     * @since 4.3.0
798
     */
799
    public List<DiscoverInfo> findServicesDiscoverInfo(DomainBareJid serviceName, String feature, boolean stopOnFirst,
800
                    boolean useCache, Map<? super Jid, Exception> encounteredExceptions)
801
            throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
802
        List<DiscoverInfo> serviceDiscoInfo;
803
        if (useCache) {
×
804
            serviceDiscoInfo = services.lookup(feature);
×
805
            if (serviceDiscoInfo != null) {
×
806
                return serviceDiscoInfo;
×
807
            }
808
        }
809
        serviceDiscoInfo = new LinkedList<>();
×
810
        // Send the disco packet to the server itself
811
        DiscoverInfo info;
812
        try {
813
            info = discoverInfo(serviceName);
×
814
        } catch (XMPPErrorException e) {
×
815
            if (encounteredExceptions != null) {
×
816
                encounteredExceptions.put(serviceName, e);
×
817
            }
818
            return serviceDiscoInfo;
×
819
        }
×
820
        // Check if the server supports the feature
821
        if (info.containsFeature(feature)) {
×
822
            serviceDiscoInfo.add(info);
×
823
            if (stopOnFirst) {
×
824
                if (useCache) {
×
825
                    // Cache the discovered information
826
                    services.put(feature, serviceDiscoInfo);
×
827
                }
828
                return serviceDiscoInfo;
×
829
            }
830
        }
831
        DiscoverItems items;
832
        try {
833
            // Get the disco items and send the disco packet to each server item
834
            items = discoverItems(serviceName);
×
835
        } catch (XMPPErrorException e) {
×
836
            if (encounteredExceptions != null) {
×
837
                encounteredExceptions.put(serviceName, e);
×
838
            }
839
            return serviceDiscoInfo;
×
840
        }
×
841
        for (DiscoverItems.Item item : items.getItems()) {
×
842
            Jid address = item.getEntityID();
×
843
            try {
844
                // TODO is it OK here in all cases to query without the node attribute?
845
                // MultipleRecipientManager queried initially also with the node attribute, but this
846
                // could be simply a fault instead of intentional.
847
                info = discoverInfo(address);
×
848
            }
849
            catch (XMPPErrorException | NoResponseException e) {
×
850
                if (encounteredExceptions != null) {
×
851
                    encounteredExceptions.put(address, e);
×
852
                }
853
                continue;
×
854
            }
×
855
            if (info.containsFeature(feature)) {
×
856
                serviceDiscoInfo.add(info);
×
857
                if (stopOnFirst) {
×
858
                    break;
×
859
                }
860
            }
861
        }
×
862
        if (useCache) {
×
863
            // Cache the discovered information
864
            services.put(feature, serviceDiscoInfo);
×
865
        }
866
        return serviceDiscoInfo;
×
867
    }
868

869
    /**
870
     * Find all services under the users service that provide a given feature.
871
     *
872
     * @param feature the feature to search for
873
     * @param stopOnFirst if true, stop searching after the first service was found
874
     * @param useCache if true, query a cache first to avoid network I/O
875
     * @return a possible empty list of services providing the given feature
876
     * @throws NoResponseException if there was no response from the remote entity.
877
     * @throws XMPPErrorException if there was an XMPP error returned.
878
     * @throws NotConnectedException if the XMPP connection is not connected.
879
     * @throws InterruptedException if the calling thread was interrupted.
880
     */
881
    public List<DomainBareJid> findServices(String feature, boolean stopOnFirst, boolean useCache) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
882
        List<DiscoverInfo> services = findServicesDiscoverInfo(feature, stopOnFirst, useCache);
×
883
        List<DomainBareJid> res = new ArrayList<>(services.size());
×
884
        for (DiscoverInfo info : services) {
×
885
            res.add(info.getFrom().asDomainBareJid());
×
886
        }
×
887
        return res;
×
888
    }
889

890
    public DomainBareJid findService(String feature, boolean useCache, String category, String type)
891
                    throws NoResponseException, XMPPErrorException, NotConnectedException,
892
                    InterruptedException {
893
        boolean noCategory = StringUtils.isNullOrEmpty(category);
×
894
        boolean noType = StringUtils.isNullOrEmpty(type);
×
895
        if (noType != noCategory) {
×
896
            throw new IllegalArgumentException("Must specify either both, category and type, or none");
×
897
        }
898

899
        List<DiscoverInfo> services = findServicesDiscoverInfo(feature, false, useCache);
×
900
        if (services.isEmpty()) {
×
901
            return null;
×
902
        }
903

904
        if (!noCategory && !noType) {
×
905
            for (DiscoverInfo info : services) {
×
906
                if (info.hasIdentity(category, type)) {
×
907
                    return info.getFrom().asDomainBareJid();
×
908
                }
909
            }
×
910
        }
911

912
        return services.get(0).getFrom().asDomainBareJid();
×
913
    }
914

915
    public DomainBareJid findService(String feature, boolean useCache) throws NoResponseException,
916
                    XMPPErrorException, NotConnectedException, InterruptedException {
917
        return findService(feature, useCache, null, null);
×
918
    }
919

920
    public boolean addEntityCapabilitiesChangedListener(EntityCapabilitiesChangedListener entityCapabilitiesChangedListener) {
921
        return entityCapabilitiesChangedListeners.add(entityCapabilitiesChangedListener);
1✔
922
    }
923

924
    public boolean removeEntityCapabilitiesChangedListener(EntityCapabilitiesChangedListener entityCapabilitiesChangedListener) {
925
        return entityCapabilitiesChangedListeners.remove(entityCapabilitiesChangedListener);
×
926
    }
927

928
    private static final int RENEW_ENTITY_CAPS_DELAY_MILLIS = 25;
929

930
    private ScheduledAction renewEntityCapsScheduledAction;
931

932
    private final AtomicInteger renewEntityCapsPerformed = new AtomicInteger();
1✔
933
    private int renewEntityCapsRequested = 0;
1✔
934
    private int scheduledRenewEntityCapsAvoided = 0;
1✔
935

936
    /**
937
     * Notify the {@link EntityCapabilitiesChangedListener} about changed capabilities.
938
     */
939
    private synchronized void renewEntityCapsVersion() {
940
        if (entityCapabilitiesChangedListeners.isEmpty()) {
1✔
941
            return;
1✔
942
        }
943

944
        renewEntityCapsRequested++;
1✔
945
        if (renewEntityCapsScheduledAction != null) {
1✔
946
            boolean canceled = renewEntityCapsScheduledAction.cancel();
1✔
947
            if (canceled) {
1✔
948
                scheduledRenewEntityCapsAvoided++;
1✔
949
            }
950
        }
951

952
        renewEntityCapsScheduledAction = scheduleBlocking(() -> {
1✔
953
            final XMPPConnection connection = connection();
1✔
954
            if (connection == null) {
1✔
955
                return;
×
956
            }
957

958
            renewEntityCapsPerformed.incrementAndGet();
1✔
959

960
            DiscoverInfoBuilder discoverInfoBuilder = DiscoverInfo.builder("synthetized-disco-info-response")
1✔
961
                            .ofType(IQ.Type.result);
1✔
962
            addDiscoverInfoTo(discoverInfoBuilder);
1✔
963
            DiscoverInfo synthesizedDiscoveryInfo = discoverInfoBuilder.build();
1✔
964

965
            for (EntityCapabilitiesChangedListener entityCapabilitiesChangedListener : entityCapabilitiesChangedListeners) {
1✔
966
                entityCapabilitiesChangedListener.onEntityCapabilitiesChanged(synthesizedDiscoveryInfo);
1✔
967
            }
1✔
968

969
            // Re-send the last sent presence, and let the stanza interceptor
970
            // add a <c/> node to it.
971
            // See http://xmpp.org/extensions/xep-0115.html#advertise
972
            // We only send a presence packet if there was already one send
973
            // to respect ConnectionConfiguration.isSendPresence()
974
            final Presence presenceSend = this.presenceSend;
1✔
975
            if (connection.isAuthenticated() && presenceSend != null) {
1✔
976
                Presence presence = presenceSend.asBuilder(connection).build();
×
977
                try {
978
                    connection.sendStanza(presence);
×
979
                }
980
                catch (InterruptedException | NotConnectedException e) {
×
981
                    LOGGER.log(Level.WARNING, "Could could not update presence with caps info", e);
×
982
                }
×
983
            }
984
        }, RENEW_ENTITY_CAPS_DELAY_MILLIS, TimeUnit.MILLISECONDS);
1✔
985
    }
1✔
986

987
    public static void addDiscoInfoLookupShortcutMechanism(DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism) {
988
        synchronized (discoInfoLookupShortcutMechanisms) {
1✔
989
            discoInfoLookupShortcutMechanisms.add(discoInfoLookupShortcutMechanism);
1✔
990
            Collections.sort(discoInfoLookupShortcutMechanisms);
1✔
991
        }
1✔
992
    }
1✔
993

994
    public static void removeDiscoInfoLookupShortcutMechanism(DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism) {
995
        synchronized (discoInfoLookupShortcutMechanisms) {
×
996
            discoInfoLookupShortcutMechanisms.remove(discoInfoLookupShortcutMechanism);
×
997
        }
×
998
    }
×
999

1000
    public synchronized Stats getStats() {
1001
        return new Stats(this);
×
1002
    }
1003

1004
    public static final class Stats extends AbstractStats {
1005

1006
        public final int renewEntityCapsRequested;
1007
        public final int renewEntityCapsPerformed;
1008
        public final int scheduledRenewEntityCapsAvoided;
1009

1010
        private Stats(ServiceDiscoveryManager serviceDiscoveryManager) {
×
1011
            renewEntityCapsRequested = serviceDiscoveryManager.renewEntityCapsRequested;
×
1012
            renewEntityCapsPerformed = serviceDiscoveryManager.renewEntityCapsPerformed.get();
×
1013
            scheduledRenewEntityCapsAvoided = serviceDiscoveryManager.scheduledRenewEntityCapsAvoided;
×
1014
        }
×
1015

1016
        @Override
1017
        public void appendStatsTo(ExtendedAppendable appendable) throws IOException {
1018
            StringUtils.appendHeading(appendable, "ServiceDiscoveryManager stats", '#').append('\n');
×
1019
            appendable.append("renew-entitycaps-requested: ").append(renewEntityCapsRequested).append('\n');
×
1020
            appendable.append("renew-entitycaps-performed: ").append(renewEntityCapsPerformed).append('\n');
×
1021
            appendable.append("scheduled-renew-entitycaps-avoided: ").append(scheduledRenewEntityCapsAvoided).append('\n');
×
1022
        }
×
1023

1024
    }
1025
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc