/** * $RCSfile$ * $Revision: 12580 $ * $Date: 2011-08-17 22:44:41 -0500 (Wed, 17 Aug 2011) $ * * 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 java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.jivesoftware.smack.Connection; import org.jivesoftware.smack.ConnectionCreationListener; import org.jivesoftware.smack.ConnectionListener; import org.jivesoftware.smack.PacketCollector; import org.jivesoftware.smack.PacketListener; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.XMPPException; 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.XMPPError; import org.jivesoftware.smackx.packet.DataForm; import org.jivesoftware.smackx.packet.DiscoverInfo; import org.jivesoftware.smackx.packet.DiscoverItems; /** * 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 String identityName = "Smack"; private static String identityType = "pc"; private static Map<Connection, ServiceDiscoveryManager> instances = new ConcurrentHashMap<Connection, ServiceDiscoveryManager>(); /** * 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() { return identityName; } /** * 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() { return identityType; } /** * 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 ServiceDiscoveryManager getInstanceFor(Connection connection) { ServiceDiscoveryManager s = instances.get(connection); if (s == null) { s = new ServiceDiscoveryManager(connection); } return s; } /** * 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) { identityName = name; } /** * 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) { identityType = type; } private final Connection connection; private final List<String> features = new ArrayList<String>(); private DataForm extendedInfo = null; private final Map<String, NodeInformationProvider> nodeInformationProviders = new ConcurrentHashMap<String, NodeInformationProvider>(); // Create a new ServiceDiscoveryManager on every established connection static { Connection .addConnectionCreationListener(new ConnectionCreationListener() { @Override public void connectionCreated(Connection connection) { try { new ServiceDiscoveryManager(connection); } catch (final Exception e) { } } }); } /** * 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. * * @param connection * the connection to which a ServiceDiscoveryManager is going to * be created. */ public ServiceDiscoveryManager(Connection connection) { this.connection = connection; if (!instances.containsKey(connection)) { init(); } else { throw new RuntimeException("Connection already exists"); } } /** * 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); } } /** * 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 { final DiscoverInfo info = discoverInfo(entityID); return info.containsFeature("http://jabber.org/protocol/disco#publish"); } /** * Returns the discovered information 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 DiscoverInfo discoverInfo(String entityID) throws XMPPException { 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. * * @param entityID * the address of the XMPP entity. * @param node * the 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 { // Discover the entity's info final DiscoverInfo disco = new DiscoverInfo(); disco.setType(IQ.Type.GET); disco.setTo(entityID); disco.setNode(node); // Create a packet collector to listen for a response. final PacketCollector collector = connection .createPacketCollector(new PacketIDFilter(disco.getPacketID())); connection.sendPacket(disco); // Wait up to 5 seconds for a result. final 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 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 { // Discover the entity's items final DiscoverItems disco = new DiscoverItems(); disco.setType(IQ.Type.GET); disco.setTo(entityID); disco.setNode(node); // Create a packet collector to listen for a response. final PacketCollector collector = connection .createPacketCollector(new PacketIDFilter(disco.getPacketID())); connection.sendPacket(disco); // Wait up to 5 seconds for a result. final 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 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 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); } /** * 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); } } /** * Initializes the packet listeners of the connection that will answer to * any service discovery request. */ private void init() { // Register the new instance and associate it with the connection instances.put(connection, this); // Add a listener to the connection that removes the registered instance // when // the connection is closed connection.addConnectionListener(new ConnectionListener() { @Override public void connectionClosed() { // Unregister this instance since the connection has been closed instances.remove(connection); } @Override public void connectionClosedOnError(Exception e) { // ignore } @Override public void reconnectingIn(int seconds) { // ignore } @Override public void reconnectionFailed(Exception e) { // ignore } @Override public void reconnectionSuccessful() { // ignore } }); // Listen for disco#items requests and answer with an empty result PacketFilter packetFilter = new PacketTypeFilter(DiscoverItems.class); PacketListener packetListener = new PacketListener() { @Override public void processPacket(Packet packet) { final 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) { final 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. final NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverItems .getNode()); if (nodeInformationProvider != null) { // Specified node was found final List<DiscoverItems.Item> items = nodeInformationProvider .getNodeItems(); if (items != null) { for (final DiscoverItems.Item item : items) { response.addItem(item); } } } 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() { @Override public void processPacket(Packet packet) { final 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) { final 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 if (discoverInfo.getNode() == null) { // Set this client identity final DiscoverInfo.Identity identity = new DiscoverInfo.Identity( "client", getIdentityName()); identity.setType(getIdentityType()); response.addIdentity(identity); // Add the registered features to the response synchronized (features) { for (final Iterator<String> it = getFeatures(); it .hasNext();) { response.addFeature(it.next()); } if (extendedInfo != null) { response.addExtension(extendedInfo); } } } else { // Disco#info was sent to a node. Check if we have // information of the // specified node final NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverInfo .getNode()); if (nodeInformationProvider != null) { // Node was found. Add node features final List<String> features = nodeInformationProvider .getNodeFeatures(); if (features != null) { for (final String feature : features) { response.addFeature(feature); } } // Add node identities final List<DiscoverInfo.Identity> identities = nodeInformationProvider .getNodeIdentities(); if (identities != null) { for (final DiscoverInfo.Identity identity : identities) { response.addIdentity(identity); } } } 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); } /** * 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 { discoverItems.setType(IQ.Type.SET); discoverItems.setTo(entityID); discoverItems.setNode(node); // Create a packet collector to listen for a response. final PacketCollector collector = connection .createPacketCollector(new PacketIDFilter(discoverItems .getPacketID())); connection.sendPacket(discoverItems); // Wait up to 5 seconds for a result. final 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()); } } /** * Removes the dataform 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; } /** * 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); } } /** * 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); } /** * 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; } /** * 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); } }