diff options
Diffstat (limited to 'src/org/jivesoftware/smackx/ServiceDiscoveryManager.java')
-rw-r--r-- | src/org/jivesoftware/smackx/ServiceDiscoveryManager.java | 708 |
1 files changed, 708 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java b/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java new file mode 100644 index 0000000..9e31f67 --- /dev/null +++ b/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java @@ -0,0 +1,708 @@ +/** + * $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.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 = + new ConcurrentHashMap<Connection, ServiceDiscoveryManager>(); + + private 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) { + new ServiceDiscoveryManager(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. + * + * @param connection the connection to which a ServiceDiscoveryManager is going to be created. + */ + public ServiceDiscoveryManager(Connection connection) { + this.connection = connection; + + init(); + } + + /** + * 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) { + return 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() { + 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); + } + + /** + * 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); + + addFeature(DiscoverInfo.NAMESPACE); + addFeature(DiscoverItems.NAMESPACE); + + // 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) { + // ignore + } + + public void reconnectionFailed(Exception e) { + // ignore + } + + public void reconnectingIn(int seconds) { + // ignore + } + + 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() { + 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()); + 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) { + 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); + } + + /** + * 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 { + // 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 { + // 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 { + 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(); + } +} |