/** * $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 DiscoverInfo.Identity defaultIdentity = new Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE); private Set<DiscoverInfo.Identity> identities = new HashSet<DiscoverInfo.Identity>(); private DiscoverInfo.Identity identity = defaultIdentity; 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); } }); } /** * Set the default identity all new connections will have. If unchanged the default identity is an * identity where category is set to 'client', type is set to 'pc' and name is set to 'Smack'. * * @param identity */ public static void setDefaultIdentity(DiscoverInfo.Identity identity) { defaultIdentity = identity; } /** * 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 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 String getIdentityName() { return identity.getName(); } /** * 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 void setIdentityName(String name) { identity.setName(name); renewEntityCapsVersion(); } /** * 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 String getIdentityType() { return identity.getType(); } /** * 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 void setIdentityType(String type) { identity.setType(type); renewEntityCapsVersion(); } /** * Add an identity to the client. * * @param identity */ public void addIdentity(DiscoverInfo.Identity identity) { identities.add(identity); renewEntityCapsVersion(); } /** * Remove an identity from the client. Note that the client needs at least one identity, the default identity, which * can not be removed. * * @param identity * @return true, if successful. Otherwise the default identity was given. */ public boolean removeIdentity(DiscoverInfo.Identity identity) { if (identity.equals(this.identity)) return false; identities.remove(identity); renewEntityCapsVersion(); return true; } /** * Returns all identities of this client as unmodifiable Collection * * @return */ public Set<DiscoverInfo.Identity> getIdentities() { Set<Identity> res = new HashSet<Identity>(identities); // Add the default identity that must exist res.add(defaultIdentity); return Collections.unmodifiableSet(res); } /** * 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; } /** * 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(getIdentities()); // 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(); } }