/** * $RCSfile$ * $Revision$ * $Date$ * * Copyright (C) 2002-2003 Jive Software. All rights reserved. * ==================================================================== * The Jive Software License (based on Apache Software License, Version 1.1) * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, * if any, must include the following acknowledgment: * "This product includes software developed by * Jive Software (http://www.jivesoftware.com)." * Alternately, this acknowledgment may appear in the software itself, * if and wherever such third-party acknowledgments normally appear. * * 4. The names "Smack" and "Jive Software" must not be used to * endorse or promote products derived from this software without * prior written permission. For written permission, please * contact webmaster@jivesoftware.com. * * 5. Products derived from this software may not be called "Smack", * nor may "Smack" appear in their name, without prior written * permission of Jive Software. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL JIVE SOFTWARE OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== */ package org.jivesoftware.smackx; import java.util.*; import org.jivesoftware.smack.*; import org.jivesoftware.smack.filter.*; import org.jivesoftware.smack.packet.*; import org.jivesoftware.smackx.packet.*; /** * Manages discovery of services in XMPP entities. This class provides: *
    *
  1. A registry of supported features in this XMPP entity. *
  2. Automatic response when this XMPP entity is queried for information. *
  3. Ability to discover items and information of remote XMPP entities. *
  4. Ability to publish publicly available items. *
* * @author Gaston Dombiak */ public class ServiceDiscoveryManager { private static String identityName = "Smack"; private static String identityType = "pc"; private static Map instances = new Hashtable(); private XMPPConnection connection; private List features = new ArrayList(); private Map nodeInformationProviders = new Hashtable(); // Create a new ServiceDiscoveryManager on every established connection static { XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() { public void connectionEstablished(XMPPConnection connection) { new ServiceDiscoveryManager(connection); } }); } /** * Creates a new ServiceDiscoveryManager for a given XMPPConnection. 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(XMPPConnection connection) { this.connection = connection; init(); } /** * Returns the ServiceDiscoveryManager instance associated with a given XMPPConnection. * * @param connection the connection used to look for the proper ServiceDiscoveryManager. * @return the ServiceDiscoveryManager associated with a given XMPPConnection. */ public static ServiceDiscoveryManager getInstanceFor(XMPPConnection connection) { return (ServiceDiscoveryManager) instances.get(connection); } /** * 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; } /** * 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; } /** * 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: Jabber::Registrar. * * @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; } /** * 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: Jabber::Registrar. * * @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; } /** * 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() { public void connectionClosed() { // Unregister this instance since the connection has been closed instances.remove(connection); } public void connectionClosedOnError(Exception e) { // Unregister this instance since the connection has been closed instances.remove(connection); } }); // 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) { 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()); // Add the defined items related to the requested node. Look for // the NodeInformationProvider associated with the requested node. if (getNodeInformationProvider(discoverItems.getNode()) != null) { Iterator items = getNodeInformationProvider(discoverItems.getNode()).getNodeItems(); while (items.hasNext()) { response.addItem((DiscoverItems.Item) items.next()); } } 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) { 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()); // Add the client's identity and features only if "node" is null if (discoverInfo.getNode() == null) { // Set this client identity DiscoverInfo.Identity identity = new DiscoverInfo.Identity("client", getIdentityName()); identity.setType(getIdentityType()); response.addIdentity(identity); // Add the registered features to the response synchronized (features) { for (Iterator it = getFeatures(); it.hasNext();) { response.addFeature((String) it.next()); } } } else { // Return an error since a client doesn't have nodes response.setType(IQ.Type.ERROR); response.setError(new XMPPError(404, "item-not-found")); } connection.sendPacket(response); } } }; connection.addPacketListener(packetListener, packetFilter); } /** * Returns the NodeInformationProvider responsible for providing information * (ie items) related to a given node or 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 (NodeInformationProvider) 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. * * @return an Iterator on the supported features by this XMPP entity. */ public Iterator getFeatures() { synchronized (features) { return Collections.unmodifiableList(new ArrayList(features)).iterator(); } } /** * 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. * * @param feature the feature to register as supported. */ public void addFeature(String feature) { synchronized (features) { features.add(feature); } } /** * Removes the specified feature from the supported features by this XMPP entity. * * @param feature the feature to remove from the supported features. */ public void removeFeature(String feature) { synchronized (features) { features.remove(feature); } } /** * 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); } } /** * 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 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 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 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 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 { 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()); } } }