1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-06-27 14:04:52 +02:00
Smack/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java
Florian Schmaus 44c3214539 SMACK-460 ServiceDiscoveryManager constructor usage in
connectionCreated()

ServiceDiscoveryManager should not use the constructor in
connectionCreated(), it may cause duplicate SDMs for the same connection
and prevents the interaction with SDM before the connection is
established. Instead getInstanceFor() is now used.

git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_2@13792 b35dd754-fafc-0310-a699-88a17e54d16e
2013-10-26 23:53:04 +00:00

695 lines
29 KiB
Java

/**
* $RCSfile$
* $Revision$
* $Date$
*
* Copyright 2003-2007 Jive Software.
*
* All rights reserved. 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;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
import org.jivesoftware.smackx.packet.DiscoverItems;
import org.jivesoftware.smackx.packet.DataForm;
import java.lang.ref.WeakReference;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages discovery of services in XMPP entities. This class provides:
* <ol>
* <li>A registry of supported features in this XMPP entity.
* <li>Automatic response when this XMPP entity is queried for information.
* <li>Ability to discover items and information of remote XMPP entities.
* <li>Ability to publish publicly available items.
* </ol>
*
* @author Gaston Dombiak
*/
public class ServiceDiscoveryManager {
private static final String DEFAULT_IDENTITY_NAME = "Smack";
private static final String DEFAULT_IDENTITY_CATEGORY = "client";
private static final String DEFAULT_IDENTITY_TYPE = "pc";
private static List<DiscoverInfo.Identity> identities = new LinkedList<DiscoverInfo.Identity>();
private EntityCapsManager capsManager;
private static Map<Connection, ServiceDiscoveryManager> instances =
Collections.synchronizedMap(new WeakHashMap<Connection, ServiceDiscoveryManager>());
private WeakReference<Connection> connection;
private final Set<String> features = new HashSet<String>();
private DataForm extendedInfo = null;
private Map<String, NodeInformationProvider> nodeInformationProviders =
new ConcurrentHashMap<String, NodeInformationProvider>();
// Create a new ServiceDiscoveryManager on every established connection
static {
Connection.addConnectionCreationListener(new ConnectionCreationListener() {
public void connectionCreated(Connection connection) {
getInstanceFor(connection);
}
});
identities.add(new Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE));
}
/**
* Creates a new ServiceDiscoveryManager for a given Connection. This means that the
* service manager will respond to any service discovery request that the connection may
* receive.
*
* @deprecated use {@link #getInstanceFor(connection)} instead
* @param connection the connection to which a ServiceDiscoveryManager is going to be created.
*/
@Deprecated
public ServiceDiscoveryManager(Connection connection) {
this.connection = new WeakReference<Connection>(connection);
// Register the new instance and associate it with the connection
instances.put(connection, this);
addFeature(DiscoverInfo.NAMESPACE);
addFeature(DiscoverItems.NAMESPACE);
// Listen for disco#items requests and answer with an empty result
PacketFilter packetFilter = new PacketTypeFilter(DiscoverItems.class);
PacketListener packetListener = new PacketListener() {
public void processPacket(Packet packet) {
Connection connection = ServiceDiscoveryManager.this.connection.get();
if (connection == null) return;
DiscoverItems discoverItems = (DiscoverItems) packet;
// Send back the items defined in the client if the request is of type GET
if (discoverItems != null && discoverItems.getType() == IQ.Type.GET) {
DiscoverItems response = new DiscoverItems();
response.setType(IQ.Type.RESULT);
response.setTo(discoverItems.getFrom());
response.setPacketID(discoverItems.getPacketID());
response.setNode(discoverItems.getNode());
// Add the defined items related to the requested node. Look for
// the NodeInformationProvider associated with the requested node.
NodeInformationProvider nodeInformationProvider =
getNodeInformationProvider(discoverItems.getNode());
if (nodeInformationProvider != null) {
// Specified node was found, add node items
response.addItems(nodeInformationProvider.getNodeItems());
// Add packet extensions
response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
} else if(discoverItems.getNode() != null) {
// Return <item-not-found/> error since client doesn't contain
// the specified node
response.setType(IQ.Type.ERROR);
response.setError(new XMPPError(XMPPError.Condition.item_not_found));
}
connection.sendPacket(response);
}
}
};
connection.addPacketListener(packetListener, packetFilter);
// Listen for disco#info requests and answer the client's supported features
// To add a new feature as supported use the #addFeature message
packetFilter = new PacketTypeFilter(DiscoverInfo.class);
packetListener = new PacketListener() {
public void processPacket(Packet packet) {
Connection connection = ServiceDiscoveryManager.this.connection.get();
if (connection == null) return;
DiscoverInfo discoverInfo = (DiscoverInfo) packet;
// Answer the client's supported features if the request is of the GET type
if (discoverInfo != null && discoverInfo.getType() == IQ.Type.GET) {
DiscoverInfo response = new DiscoverInfo();
response.setType(IQ.Type.RESULT);
response.setTo(discoverInfo.getFrom());
response.setPacketID(discoverInfo.getPacketID());
response.setNode(discoverInfo.getNode());
// Add the client's identity and features only if "node" is null
// and if the request was not send to a node. If Entity Caps are
// enabled the client's identity and features are may also added
// if the right node is chosen
if (discoverInfo.getNode() == null) {
addDiscoverInfoTo(response);
}
else {
// Disco#info was sent to a node. Check if we have information of the
// specified node
NodeInformationProvider nodeInformationProvider =
getNodeInformationProvider(discoverInfo.getNode());
if (nodeInformationProvider != null) {
// Node was found. Add node features
response.addFeatures(nodeInformationProvider.getNodeFeatures());
// Add node identities
response.addIdentities(nodeInformationProvider.getNodeIdentities());
// Add packet extensions
response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
}
else {
// Return <item-not-found/> error since specified node was not found
response.setType(IQ.Type.ERROR);
response.setError(new XMPPError(XMPPError.Condition.item_not_found));
}
}
connection.sendPacket(response);
}
}
};
connection.addPacketListener(packetListener, packetFilter);
}
/**
* Returns the ServiceDiscoveryManager instance associated with a given Connection.
*
* @param connection the connection used to look for the proper ServiceDiscoveryManager.
* @return the ServiceDiscoveryManager associated with a given Connection.
*/
public static synchronized ServiceDiscoveryManager getInstanceFor(Connection connection) {
ServiceDiscoveryManager sdm = instances.get(connection);
if (sdm == null) {
sdm = new ServiceDiscoveryManager(connection);
}
return sdm;
}
/**
* Returns the name of the client that will be returned when asked for the client identity
* in a disco request. The name could be any value you need to identity this client.
*
* @return the name of the client that will be returned when asked for the client identity
* in a disco request.
*/
public static String getIdentityName() {
DiscoverInfo.Identity identity = identities.get(0);
if (identity != null) {
return identity.getName();
} else {
return null;
}
}
/**
* Sets the name of the client that will be returned when asked for the client identity
* in a disco request. The name could be any value you need to identity this client.
*
* @param name the name of the client that will be returned when asked for the client identity
* in a disco request.
*/
public static void setIdentityName(String name) {
DiscoverInfo.Identity identity = identities.remove(0);
identity = new DiscoverInfo.Identity(DEFAULT_IDENTITY_CATEGORY, name, DEFAULT_IDENTITY_TYPE);
identities.add(identity);
}
/**
* Returns the type of client that will be returned when asked for the client identity in a
* disco request. The valid types are defined by the category client. Follow this link to learn
* the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>.
*
* @return the type of client that will be returned when asked for the client identity in a
* disco request.
*/
public static String getIdentityType() {
DiscoverInfo.Identity identity = identities.get(0);
if (identity != null) {
return identity.getType();
} else {
return null;
}
}
/**
* Sets the type of client that will be returned when asked for the client identity in a
* disco request. The valid types are defined by the category client. Follow this link to learn
* the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>.
*
* @param type the type of client that will be returned when asked for the client identity in a
* disco request.
*/
public static void setIdentityType(String type) {
DiscoverInfo.Identity identity = identities.get(0);
if (identity != null) {
identity.setType(type);
} else {
identity = new DiscoverInfo.Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, type);
identities.add(identity);
}
}
/**
* Returns all identities of this client as unmodifiable Collection
*
* @return
*/
public static List<DiscoverInfo.Identity> getIdentities() {
return Collections.unmodifiableList(identities);
}
/**
* Add discover info response data.
*
* @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol; Example 2</a>
*
* @param response the discover info response packet
*/
public void addDiscoverInfoTo(DiscoverInfo response) {
// First add the identities of the connection
response.addIdentities(identities);
// Add the registered features to the response
synchronized (features) {
for (Iterator<String> it = getFeatures(); it.hasNext();) {
response.addFeature(it.next());
}
response.addExtension(extendedInfo);
}
}
/**
* Returns the NodeInformationProvider responsible for providing information
* (ie items) related to a given node or <tt>null</null> if none.<p>
*
* 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.<p>
*
* 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.
*
* @return an Iterator on the supported features by this XMPP entity.
*/
public Iterator<String> getFeatures() {
synchronized (features) {
return Collections.unmodifiableList(new ArrayList<String>(features)).iterator();
}
}
/**
* Returns the supported features by this XMPP entity.
*
* @return a copy of the List on the supported features by this XMPP entity.
*/
public List<String> getFeaturesList() {
synchronized (features) {
return new LinkedList<String>(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.<p>
*
* Since no packet 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 void addFeature(String feature) {
synchronized (features) {
features.add(feature);
renewEntityCapsVersion();
}
}
/**
* Removes the specified feature from the supported features by this XMPP entity.<p>
*
* Since no packet 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 void removeFeature(String feature) {
synchronized (features) {
features.remove(feature);
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 boolean includesFeature(String feature) {
synchronized (features) {
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.
* <p>
*
* Since no packet 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 void setExtendedInfo(DataForm info) {
extendedInfo = info;
renewEntityCapsVersion();
}
/**
* Returns the data form that is set as extended information for this Service Discovery instance (XEP-0128)
*
* @see <a href="http://xmpp.org/extensions/xep-0128.html">XEP-128: Service Discovery Extensions</a>
* @return
*/
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
*/
public List<PacketExtension> getExtendedInfoAsList() {
List<PacketExtension> res = null;
if (extendedInfo != null) {
res = new ArrayList<PacketExtension>(1);
res.add(extendedInfo);
}
return res;
}
/**
* Removes the data form containing extended service discovery information
* from the information returned by this XMPP entity.<p>
*
* Since no packet is actually sent to the server it is safe to perform this
* operation before logging to the server.
*/
public void removeExtendedInfo() {
extendedInfo = null;
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 XMPPException if the operation failed for some reason.
*/
public DiscoverInfo discoverInfo(String entityID) throws XMPPException {
if (entityID == null)
return discoverInfo(null, null);
// Check if the have it cached in the Entity Capabilities Manager
DiscoverInfo info = EntityCapsManager.getDiscoverInfoByUser(entityID);
if (info != null) {
// We were able to retrieve the information from Entity Caps and
// avoided a disco request, hurray!
return info;
}
// Try to get the newest node#version if it's known, otherwise null is
// returned
EntityCapsManager.NodeVerHash nvh = EntityCapsManager.getNodeVerHashByJid(entityID);
// Discover by requesting the information from the remote entity
// Note that wee need to use NodeVer as argument for Node if it exists
info = discoverInfo(entityID, nvh != null ? nvh.getNodeVer() : null);
// If the node version is known, store the new entry.
if (nvh != null) {
if (EntityCapsManager.verifyDiscoverInfoVersion(nvh.getVer(), nvh.getHash(), info))
EntityCapsManager.addDiscoverInfoByNode(nvh.getNodeVer(), info);
}
return info;
}
/**
* 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 <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a>
* @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a>
*
* @param entityID the address of the XMPP entity.
* @param node the optional attribute that supplements the 'jid' attribute.
* @return the discovered information.
* @throws XMPPException if the operation failed for some reason.
*/
public DiscoverInfo discoverInfo(String entityID, String node) throws XMPPException {
Connection connection = ServiceDiscoveryManager.this.connection.get();
if (connection == null) throw new XMPPException("Connection instance already gc'ed");
// Discover the entity's info
DiscoverInfo disco = new DiscoverInfo();
disco.setType(IQ.Type.GET);
disco.setTo(entityID);
disco.setNode(node);
// Create a packet collector to listen for a response.
PacketCollector collector =
connection.createPacketCollector(new PacketIDFilter(disco.getPacketID()));
connection.sendPacket(disco);
// Wait up to 5 seconds for a result.
IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
collector.cancel();
if (result == null) {
throw new XMPPException("No response from the server.");
}
if (result.getType() == IQ.Type.ERROR) {
throw new XMPPException(result.getError());
}
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 XMPPException if the operation failed for some reason.
*/
public DiscoverItems discoverItems(String entityID) throws XMPPException {
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 XMPPException if the operation failed for some reason.
*/
public DiscoverItems discoverItems(String entityID, String node) throws XMPPException {
Connection connection = ServiceDiscoveryManager.this.connection.get();
if (connection == null) throw new XMPPException("Connection instance already gc'ed");
// Discover the entity's items
DiscoverItems disco = new DiscoverItems();
disco.setType(IQ.Type.GET);
disco.setTo(entityID);
disco.setNode(node);
// Create a packet collector to listen for a response.
PacketCollector collector =
connection.createPacketCollector(new PacketIDFilter(disco.getPacketID()));
connection.sendPacket(disco);
// Wait up to 5 seconds for a result.
IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
collector.cancel();
if (result == null) {
throw new XMPPException("No response from the server.");
}
if (result.getType() == IQ.Type.ERROR) {
throw new XMPPException(result.getError());
}
return (DiscoverItems) result;
}
/**
* Returns true if the server supports publishing of items. A client may wish to publish items
* to the server so that the server can provide items associated to the client. These items will
* be returned by the server whenever the server receives a disco request targeted to the bare
* address of the client (i.e. user@host.com).
*
* @param entityID the address of the XMPP entity.
* @return true if the server supports publishing of items.
* @throws XMPPException if the operation failed for some reason.
*/
public boolean canPublishItems(String entityID) throws XMPPException {
DiscoverInfo info = discoverInfo(entityID);
return canPublishItems(info);
}
/**
* Returns true if the server supports publishing of items. A client may wish to publish items
* to the server so that the server can provide items associated to the client. These items will
* be returned by the server whenever the server receives a disco request targeted to the bare
* address of the client (i.e. user@host.com).
*
* @param DiscoverInfo the discover info packet to check.
* @return true if the server supports publishing of items.
*/
public static boolean canPublishItems(DiscoverInfo info) {
return info.containsFeature("http://jabber.org/protocol/disco#publish");
}
/**
* Publishes new items to a parent entity. The item elements to publish MUST have at least
* a 'jid' attribute specifying the Entity ID of the item, and an action attribute which
* specifies the action being taken for that item. Possible action values are: "update" and
* "remove".
*
* @param entityID the address of the XMPP entity.
* @param discoverItems the DiscoveryItems to publish.
* @throws XMPPException if the operation failed for some reason.
*/
public void publishItems(String entityID, DiscoverItems discoverItems)
throws XMPPException {
publishItems(entityID, null, discoverItems);
}
/**
* Publishes new items to a parent entity and node. The item elements to publish MUST have at
* least a 'jid' attribute specifying the Entity ID of the item, and an action attribute which
* specifies the action being taken for that item. Possible action values are: "update" and
* "remove".
*
* @param entityID the address of the XMPP entity.
* @param node the attribute that supplements the 'jid' attribute.
* @param discoverItems the DiscoveryItems to publish.
* @throws XMPPException if the operation failed for some reason.
*/
public void publishItems(String entityID, String node, DiscoverItems discoverItems)
throws XMPPException {
Connection connection = ServiceDiscoveryManager.this.connection.get();
if (connection == null) throw new XMPPException("Connection instance already gc'ed");
discoverItems.setType(IQ.Type.SET);
discoverItems.setTo(entityID);
discoverItems.setNode(node);
// Create a packet collector to listen for a response.
PacketCollector collector =
connection.createPacketCollector(new PacketIDFilter(discoverItems.getPacketID()));
connection.sendPacket(discoverItems);
// Wait up to 5 seconds for a result.
IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
collector.cancel();
if (result == null) {
throw new XMPPException("No response from the server.");
}
if (result.getType() == IQ.Type.ERROR) {
throw new XMPPException(result.getError());
}
}
/**
* Entity Capabilities
*/
/**
* Loads the ServiceDiscoveryManager with an EntityCapsManger
* that speeds up certain lookups
* @param manager
*/
public void setEntityCapsManager(EntityCapsManager manager) {
capsManager = manager;
}
/**
* Updates the Entity Capabilities Verification String
* if EntityCaps is enabled
*/
private void renewEntityCapsVersion() {
if (capsManager != null && capsManager.entityCapsEnabled())
capsManager.updateLocalEntityCaps();
}
}