/** * * Copyright 2003-2007 Jive Software, 2018-2020 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.smackx.disco; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.ConnectionListener; import org.jivesoftware.smack.Manager; import org.jivesoftware.smack.ScheduledAction; import org.jivesoftware.smack.SmackException.NoResponseException; import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnectionRegistry; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.filter.PresenceTypeFilter; import org.jivesoftware.smack.internal.AbstractStats; import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StanzaError; import org.jivesoftware.smack.util.CollectionUtil; import org.jivesoftware.smack.util.ExtendedAppendable; import org.jivesoftware.smack.util.Objects; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.disco.packet.DiscoverInfo; import org.jivesoftware.smackx.disco.packet.DiscoverInfo.Identity; import org.jivesoftware.smackx.disco.packet.DiscoverInfoBuilder; import org.jivesoftware.smackx.disco.packet.DiscoverItems; import org.jivesoftware.smackx.xdata.packet.DataForm; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityBareJid; import org.jxmpp.jid.Jid; import org.jxmpp.util.cache.Cache; import org.jxmpp.util.cache.ExpirationCache; /** * Manages discovery of services in XMPP entities. This class provides: *
null if none.
*
* In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
* NodeInformationProvider will provide information about the rooms where the user has joined.
*
* @param node the node that contains items associated with an entity not addressable as a JID.
* @return the NodeInformationProvider responsible for providing information related
* to a given node.
*/
private NodeInformationProvider getNodeInformationProvider(String node) {
if (node == null) {
return null;
}
return nodeInformationProviders.get(node);
}
/**
* Sets the NodeInformationProvider responsible for providing information
* (ie items) related to a given node. Every time this client receives a disco request
* regarding the items of a given node, the provider associated to that node will be the
* responsible for providing the requested information.
*
* In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
* NodeInformationProvider will provide information about the rooms where the user has joined.
*
* @param node the node whose items will be provided by the NodeInformationProvider.
* @param listener the NodeInformationProvider responsible for providing items related
* to the node.
*/
public void setNodeInformationProvider(String node, NodeInformationProvider listener) {
nodeInformationProviders.put(node, listener);
}
/**
* Removes the NodeInformationProvider responsible for providing information
* (ie items) related to a given node. This means that no more information will be
* available for the specified node.
*
* In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
* NodeInformationProvider will provide information about the rooms where the user has joined.
*
* @param node the node to remove the associated NodeInformationProvider.
*/
public void removeNodeInformationProvider(String node) {
nodeInformationProviders.remove(node);
}
/**
* Returns the supported features by this XMPP entity.
*
* The result is a copied modifiable list of the original features.
*
*
* @return a List of the supported features by this XMPP entity.
*/
public synchronized List getFeatures() {
return new ArrayList<>(features);
}
/**
* Registers that a new feature is supported by this XMPP entity. When this client is
* queried for its information the registered features will be answered.
*
* Since no stanza is actually sent to the server it is safe to perform this operation
* before logging to the server. In fact, you may want to configure the supported features
* before logging to the server so that the information is already available if it is required
* upon login.
*
* @param feature the feature to register as supported.
*/
public synchronized void addFeature(String feature) {
features.add(feature);
// Notify others of a state change of SDM. In order to keep the state consistent, this
// method is synchronized
renewEntityCapsVersion();
}
/**
* Removes the specified feature from the supported features by this XMPP entity.
*
* Since no stanza is actually sent to the server it is safe to perform this operation
* before logging to the server.
*
* @param feature the feature to remove from the supported features.
*/
public synchronized void removeFeature(String feature) {
features.remove(feature);
// Notify others of a state change of SDM. In order to keep the state consistent, this
// method is synchronized
renewEntityCapsVersion();
}
/**
* Returns true if the specified feature is registered in the ServiceDiscoveryManager.
*
* @param feature the feature to look for.
* @return a boolean indicating if the specified featured is registered or not.
*/
public synchronized boolean includesFeature(String feature) {
return features.contains(feature);
}
/**
* Registers extended discovery information of this XMPP entity. When this
* client is queried for its information this data form will be returned as
* specified by XEP-0128.
*
*
* Since no stanza is actually sent to the server it is safe to perform this
* operation before logging to the server. In fact, you may want to
* configure the extended info before logging to the server so that the
* information is already available if it is required upon login.
*
* @param info the data form that contains the extend service discovery
* information.
* @deprecated use {@link #addExtendedInfo(DataForm)} instead.
*/
// TODO: Remove in Smack 4.5
@Deprecated
public synchronized void setExtendedInfo(DataForm info) {
addExtendedInfo(info);
}
/**
* Registers extended discovery information of this XMPP entity. When this
* client is queried for its information this data form will be returned as
* specified by XEP-0128.
*
*
* Since no stanza is actually sent to the server it is safe to perform this
* operation before logging to the server. In fact, you may want to
* configure the extended info before logging to the server so that the
* information is already available if it is required upon login.
*
* @param extendedInfo the data form that contains the extend service discovery information.
* @return the old data form which got replaced (if any)
* @since 4.4.0
*/
public DataForm addExtendedInfo(DataForm extendedInfo) {
String formType = extendedInfo.getFormType();
StringUtils.requireNotNullNorEmpty(formType, "The data form must have a form type set");
DataForm removedDataForm;
synchronized (this) {
removedDataForm = DataForm.remove(extendedInfos, formType);
extendedInfos.add(extendedInfo);
// Notify others of a state change of SDM. In order to keep the state consistent, this
// method is synchronized
renewEntityCapsVersion();
}
return removedDataForm;
}
/**
* Remove the extended discovery information of the given form type.
*
* @param formType the type of the data form with the extended discovery information to remove.
* @since 4.4.0
*/
public synchronized void removeExtendedInfo(String formType) {
DataForm removedForm = DataForm.remove(extendedInfos, formType);
if (removedForm != null) {
renewEntityCapsVersion();
}
}
/**
* Returns the data form as List of PacketExtensions, or null if no data form is set.
* This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider)
*
* @return the data form as List of PacketExtensions
*/
public synchronized List getExtendedInfo() {
return CollectionUtil.newListWith(extendedInfos);
}
/**
* Returns the data form as List of PacketExtensions, or null if no data form is set.
* This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider)
*
* @return the data form as List of PacketExtensions
* @deprecated use {@link #getExtendedInfo()} instead.
*/
// TODO: Remove in Smack 4.5
@Deprecated
public List getExtendedInfoAsList() {
return getExtendedInfo();
}
/**
* Removes the data form containing extended service discovery information
* from the information returned by this XMPP entity.
*
* Since no stanza is actually sent to the server it is safe to perform this
* operation before logging to the server.
*/
public synchronized void removeExtendedInfo() {
int extendedInfosCount = extendedInfos.size();
extendedInfos.clear();
if (extendedInfosCount > 0) {
// Notify others of a state change of SDM. In order to keep the state consistent, this
// method is synchronized
renewEntityCapsVersion();
}
}
/**
* Returns the discovered information of a given XMPP entity addressed by its JID.
* Use null as entityID to query the server
*
* @param entityID the address of the XMPP entity or null.
* @return the discovered information.
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NoResponseException if there was no response from the remote entity.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
public DiscoverInfo discoverInfo(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
if (entityID == null)
return discoverInfo(null, null);
synchronized (discoInfoLookupShortcutMechanisms) {
for (DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism : discoInfoLookupShortcutMechanisms) {
DiscoverInfo info = discoInfoLookupShortcutMechanism.getDiscoverInfoByUser(this, entityID);
if (info != null) {
// We were able to retrieve the information from Entity Caps and
// avoided a disco request, hurray!
return info;
}
}
}
// Last resort: Standard discovery.
return discoverInfo(entityID, null);
}
/**
* Returns the discovered information of a given XMPP entity addressed by its JID and
* note attribute. Use this message only when trying to query information which is not
* directly addressable.
*
* @see XEP-30 Basic Protocol
* @see XEP-30 Info Nodes
*
* @param entityID the address of the XMPP entity.
* @param node the optional attribute that supplements the 'jid' attribute.
* @return the discovered information.
* @throws XMPPErrorException if the operation failed for some reason.
* @throws NoResponseException if there was no response from the server.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
public DiscoverInfo discoverInfo(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
XMPPConnection connection = connection();
// Discover the entity's info
DiscoverInfo discoInfoRequest = DiscoverInfo.builder(connection)
.to(entityID)
.setNode(node)
.build();
Stanza result = connection.createStanzaCollectorAndSend(discoInfoRequest).nextResultOrThrow();
return (DiscoverInfo) result;
}
/**
* Returns the discovered items of a given XMPP entity addressed by its JID.
*
* @param entityID the address of the XMPP entity.
* @return the discovered information.
* @throws XMPPErrorException if the operation failed for some reason.
* @throws NoResponseException if there was no response from the server.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
public DiscoverItems discoverItems(Jid entityID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return discoverItems(entityID, null);
}
/**
* Returns the discovered items of a given XMPP entity addressed by its JID and
* note attribute. Use this message only when trying to query information which is not
* directly addressable.
*
* @param entityID the address of the XMPP entity.
* @param node the optional attribute that supplements the 'jid' attribute.
* @return the discovered items.
* @throws XMPPErrorException if the operation failed for some reason.
* @throws NoResponseException if there was no response from the server.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
public DiscoverItems discoverItems(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
// Discover the entity's items
DiscoverItems disco = new DiscoverItems();
disco.setType(IQ.Type.get);
disco.setTo(entityID);
disco.setNode(node);
Stanza result = connection().createStanzaCollectorAndSend(disco).nextResultOrThrow();
return (DiscoverItems) result;
}
/**
* Returns true if the server supports the given feature.
*
* @param feature TODO javadoc me please
* @return true if the server supports the given feature.
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
* @since 4.1
*/
public boolean serverSupportsFeature(CharSequence feature) throws NoResponseException, XMPPErrorException,
NotConnectedException, InterruptedException {
return serverSupportsFeatures(feature);
}
public boolean serverSupportsFeatures(CharSequence... features) throws NoResponseException,
XMPPErrorException, NotConnectedException, InterruptedException {
return serverSupportsFeatures(Arrays.asList(features));
}
public boolean serverSupportsFeatures(Collection extends CharSequence> features)
throws NoResponseException, XMPPErrorException, NotConnectedException,
InterruptedException {
return supportsFeatures(connection().getXMPPServiceDomain(), features);
}
/**
* Check if the given features are supported by the connection account. This means that the discovery information
* lookup will be performed on the bare JID of the connection managed by this ServiceDiscoveryManager.
*
* @param features the features to check
* @return true
if all features are supported by the connection account, false
otherwise
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
* @since 4.2.2
*/
public boolean accountSupportsFeatures(CharSequence... features)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return accountSupportsFeatures(Arrays.asList(features));
}
/**
* Check if the given collection of features are supported by the connection account. This means that the discovery
* information lookup will be performed on the bare JID of the connection managed by this ServiceDiscoveryManager.
*
* @param features a collection of features
* @return true
if all features are supported by the connection account, false
otherwise
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
* @since 4.2.2
*/
public boolean accountSupportsFeatures(Collection extends CharSequence> features)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
EntityBareJid accountJid = connection().getUser().asEntityBareJid();
return supportsFeatures(accountJid, features);
}
/**
* Queries the remote entity for it's features and returns true if the given feature is found.
*
* @param jid the JID of the remote entity
* @param feature TODO javadoc me please
* @return true if the entity supports the feature, false otherwise
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NoResponseException if there was no response from the remote entity.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
public boolean supportsFeature(Jid jid, CharSequence feature) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return supportsFeatures(jid, feature);
}
public boolean supportsFeatures(Jid jid, CharSequence... features) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return supportsFeatures(jid, Arrays.asList(features));
}
public boolean supportsFeatures(Jid jid, Collection extends CharSequence> features) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
DiscoverInfo result = discoverInfo(jid);
for (CharSequence feature : features) {
if (!result.containsFeature(feature)) {
return false;
}
}
return true;
}
/**
* Create a cache to hold the 25 most recently lookup services for a given feature for a period
* of 24 hours.
*/
private final Cache> services = new ExpirationCache<>(25,
24 * 60 * 60 * 1000);
/**
* Find all services under the users service that provide a given feature.
*
* @param feature the feature to search for
* @param stopOnFirst if true, stop searching after the first service was found
* @param useCache if true, query a cache first to avoid network I/O
* @return a possible empty list of services providing the given feature
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
public List findServicesDiscoverInfo(String feature, boolean stopOnFirst, boolean useCache)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
return findServicesDiscoverInfo(feature, stopOnFirst, useCache, null);
}
/**
* Find all services under the users service that provide a given feature.
*
* @param feature the feature to search for
* @param stopOnFirst if true, stop searching after the first service was found
* @param useCache if true, query a cache first to avoid network I/O
* @param encounteredExceptions an optional map which will be filled with the exceptions encountered
* @return a possible empty list of services providing the given feature
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
* @since 4.2.2
*/
public List findServicesDiscoverInfo(String feature, boolean stopOnFirst, boolean useCache, Map super Jid, Exception> encounteredExceptions)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
DomainBareJid serviceName = connection().getXMPPServiceDomain();
return findServicesDiscoverInfo(serviceName, feature, stopOnFirst, useCache, encounteredExceptions);
}
/**
* Find all services under a given service that provide a given feature.
*
* @param serviceName the service to query
* @param feature the feature to search for
* @param stopOnFirst if true, stop searching after the first service was found
* @param useCache if true, query a cache first to avoid network I/O
* @param encounteredExceptions an optional map which will be filled with the exceptions encountered
* @return a possible empty list of services providing the given feature
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
* @since 4.3.0
*/
public List findServicesDiscoverInfo(DomainBareJid serviceName, String feature, boolean stopOnFirst,
boolean useCache, Map super Jid, Exception> encounteredExceptions)
throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
List serviceDiscoInfo;
if (useCache) {
serviceDiscoInfo = services.lookup(feature);
if (serviceDiscoInfo != null) {
return serviceDiscoInfo;
}
}
serviceDiscoInfo = new LinkedList<>();
// Send the disco packet to the server itself
DiscoverInfo info;
try {
info = discoverInfo(serviceName);
} catch (XMPPErrorException e) {
if (encounteredExceptions != null) {
encounteredExceptions.put(serviceName, e);
}
return serviceDiscoInfo;
}
// Check if the server supports the feature
if (info.containsFeature(feature)) {
serviceDiscoInfo.add(info);
if (stopOnFirst) {
if (useCache) {
// Cache the discovered information
services.put(feature, serviceDiscoInfo);
}
return serviceDiscoInfo;
}
}
DiscoverItems items;
try {
// Get the disco items and send the disco packet to each server item
items = discoverItems(serviceName);
} catch (XMPPErrorException e) {
if (encounteredExceptions != null) {
encounteredExceptions.put(serviceName, e);
}
return serviceDiscoInfo;
}
for (DiscoverItems.Item item : items.getItems()) {
Jid address = item.getEntityID();
try {
// TODO is it OK here in all cases to query without the node attribute?
// MultipleRecipientManager queried initially also with the node attribute, but this
// could be simply a fault instead of intentional.
info = discoverInfo(address);
}
catch (XMPPErrorException | NoResponseException e) {
if (encounteredExceptions != null) {
encounteredExceptions.put(address, e);
}
continue;
}
if (info.containsFeature(feature)) {
serviceDiscoInfo.add(info);
if (stopOnFirst) {
break;
}
}
}
if (useCache) {
// Cache the discovered information
services.put(feature, serviceDiscoInfo);
}
return serviceDiscoInfo;
}
/**
* Find all services under the users service that provide a given feature.
*
* @param feature the feature to search for
* @param stopOnFirst if true, stop searching after the first service was found
* @param useCache if true, query a cache first to avoid network I/O
* @return a possible empty list of services providing the given feature
* @throws NoResponseException if there was no response from the remote entity.
* @throws XMPPErrorException if there was an XMPP error returned.
* @throws NotConnectedException if the XMPP connection is not connected.
* @throws InterruptedException if the calling thread was interrupted.
*/
public List findServices(String feature, boolean stopOnFirst, boolean useCache) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
List services = findServicesDiscoverInfo(feature, stopOnFirst, useCache);
List res = new ArrayList<>(services.size());
for (DiscoverInfo info : services) {
res.add(info.getFrom().asDomainBareJid());
}
return res;
}
public DomainBareJid findService(String feature, boolean useCache, String category, String type)
throws NoResponseException, XMPPErrorException, NotConnectedException,
InterruptedException {
boolean noCategory = StringUtils.isNullOrEmpty(category);
boolean noType = StringUtils.isNullOrEmpty(type);
if (noType != noCategory) {
throw new IllegalArgumentException("Must specify either both, category and type, or none");
}
List services = findServicesDiscoverInfo(feature, false, useCache);
if (services.isEmpty()) {
return null;
}
if (!noCategory && !noType) {
for (DiscoverInfo info : services) {
if (info.hasIdentity(category, type)) {
return info.getFrom().asDomainBareJid();
}
}
}
return services.get(0).getFrom().asDomainBareJid();
}
public DomainBareJid findService(String feature, boolean useCache) throws NoResponseException,
XMPPErrorException, NotConnectedException, InterruptedException {
return findService(feature, useCache, null, null);
}
public boolean addEntityCapabilitiesChangedListener(EntityCapabilitiesChangedListener entityCapabilitiesChangedListener) {
return entityCapabilitiesChangedListeners.add(entityCapabilitiesChangedListener);
}
private static final int RENEW_ENTITY_CAPS_DELAY_MILLIS = 25;
private ScheduledAction renewEntityCapsScheduledAction;
private final AtomicInteger renewEntityCapsPerformed = new AtomicInteger();
private int renewEntityCapsRequested = 0;
private int scheduledRenewEntityCapsAvoided = 0;
/**
* Notify the {@link EntityCapabilitiesChangedListener} about changed capabilities.
*/
private synchronized void renewEntityCapsVersion() {
renewEntityCapsRequested++;
if (renewEntityCapsScheduledAction != null) {
boolean canceled = renewEntityCapsScheduledAction.cancel();
if (canceled) {
scheduledRenewEntityCapsAvoided++;
}
}
final XMPPConnection connection = connection();
renewEntityCapsScheduledAction = scheduleBlocking(() -> {
renewEntityCapsPerformed.incrementAndGet();
DiscoverInfoBuilder discoverInfoBuilder = DiscoverInfo.builder("synthetized-disco-info-response")
.ofType(IQ.Type.result);
addDiscoverInfoTo(discoverInfoBuilder);
DiscoverInfo synthesizedDiscoveryInfo = discoverInfoBuilder.build();
for (EntityCapabilitiesChangedListener entityCapabilitiesChangedListener : entityCapabilitiesChangedListeners) {
entityCapabilitiesChangedListener.onEntityCapabilitiesChanged(synthesizedDiscoveryInfo);
}
// Re-send the last sent presence, and let the stanza interceptor
// add a node to it.
// See http://xmpp.org/extensions/xep-0115.html#advertise
// We only send a presence packet if there was already one send
// to respect ConnectionConfiguration.isSendPresence()
final Presence presenceSend = this.presenceSend;
if (connection.isAuthenticated() && presenceSend != null) {
Presence presence = presenceSend.asBuilder(connection).build();
try {
connection.sendStanza(presence);
}
catch (InterruptedException | NotConnectedException e) {
LOGGER.log(Level.WARNING, "Could could not update presence with caps info", e);
}
}
}, RENEW_ENTITY_CAPS_DELAY_MILLIS, TimeUnit.MILLISECONDS);
}
public static void addDiscoInfoLookupShortcutMechanism(DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism) {
synchronized (discoInfoLookupShortcutMechanisms) {
discoInfoLookupShortcutMechanisms.add(discoInfoLookupShortcutMechanism);
Collections.sort(discoInfoLookupShortcutMechanisms);
}
}
public static void removeDiscoInfoLookupShortcutMechanism(DiscoInfoLookupShortcutMechanism discoInfoLookupShortcutMechanism) {
synchronized (discoInfoLookupShortcutMechanisms) {
discoInfoLookupShortcutMechanisms.remove(discoInfoLookupShortcutMechanism);
}
}
public synchronized Stats getStats() {
return new Stats(this);
}
public static final class Stats extends AbstractStats {
public final int renewEntityCapsRequested;
public final int renewEntityCapsPerformed;
public final int scheduledRenewEntityCapsAvoided;
private Stats(ServiceDiscoveryManager serviceDiscoveryManager) {
renewEntityCapsRequested = serviceDiscoveryManager.renewEntityCapsRequested;
renewEntityCapsPerformed = serviceDiscoveryManager.renewEntityCapsPerformed.get();
scheduledRenewEntityCapsAvoided = serviceDiscoveryManager.scheduledRenewEntityCapsAvoided;
}
@Override
public void appendStatsTo(ExtendedAppendable appendable) throws IOException {
StringUtils.appendHeading(appendable, "ServiceDiscoveryManager stats", '#').append('\n');
appendable.append("renew-entitycaps-requested: ").append(renewEntityCapsRequested).append('\n');
appendable.append("renew-entitycaps-performed: ").append(renewEntityCapsPerformed).append('\n');
appendable.append("scheduled-renew-entitycaps-avoided: ").append(scheduledRenewEntityCapsAvoided).append('\n');
}
}
}