/** * * Copyright 2003-2007 Jive Software, 2018 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.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 org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.Manager; 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.iqrequest.AbstractIqRequestHandler; import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.packet.StanzaError; 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.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: *
* * 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* * 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.
*/
public synchronized void setExtendedInfo(DataForm info) {
extendedInfo = info;
// Notify others of a state change of SDM. In order to keep the state consistent, this
// method is synchronized
renewEntityCapsVersion();
}
/**
* Returns the data form that is set as extended information for this Service Discovery instance (XEP-0128).
*
* @see XEP-128: Service Discovery Extensions
* @return the data form
*/
public DataForm getExtendedInfo() {
return extendedInfo;
}
/**
* 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 List
*
* 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() {
extendedInfo = null;
// 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
* @throws NoResponseException
* @throws NotConnectedException
* @throws InterruptedException
*/
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
* @throws InterruptedException
*/
public DiscoverInfo discoverInfo(Jid entityID, String node) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
// Discover the entity's info
DiscoverInfo disco = new DiscoverInfo();
disco.setType(IQ.Type.get);
disco.setTo(entityID);
disco.setNode(node);
Stanza result = connection().createStanzaCollectorAndSend(disco).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
* @throws InterruptedException
*/
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
* @throws InterruptedException
*/
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
* @return true if the server supports the given feature.
* @throws NoResponseException
* @throws XMPPErrorException
* @throws NotConnectedException
* @throws InterruptedException
* @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
* @throws XMPPErrorException
* @throws NotConnectedException
* @throws InterruptedException
* @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
* @throws XMPPErrorException
* @throws NotConnectedException
* @throws InterruptedException
* @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
* @return true if the entity supports the feature, false otherwise
* @throws XMPPErrorException
* @throws NoResponseException
* @throws NotConnectedException
* @throws InterruptedException
*/
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