@@ -334,6 +369,17 @@ public class ServiceDiscoveryManager {
}
}
+ /**
+ * Returns the supported features by this XMPP entity.
+ *
+ * @return a copy of the List on the supported features by this XMPP entity.
+ */
+ public List getFeaturesList() {
+ synchronized (features) {
+ return new LinkedList(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.
@@ -348,6 +394,7 @@ public class ServiceDiscoveryManager {
public void addFeature(String feature) {
synchronized (features) {
features.add(feature);
+ renewEntityCapsVersion();
}
}
@@ -362,6 +409,7 @@ public class ServiceDiscoveryManager {
public void removeFeature(String feature) {
synchronized (features) {
features.remove(feature);
+ renewEntityCapsVersion();
}
}
@@ -394,10 +442,36 @@ public class ServiceDiscoveryManager {
*/
public void setExtendedInfo(DataForm info) {
extendedInfo = info;
+ renewEntityCapsVersion();
}
/**
- * Removes the dataform containing extended service discovery information
+ * Returns the data form that is set as extended information for this Service Discovery instance (XEP-0128)
+ *
+ * @see XEP-128: Service Discovery Extensions
+ * @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 getExtendedInfoAsList() {
+ List res = null;
+ if (extendedInfo != null) {
+ res = new ArrayList(1);
+ res.add(extendedInfo);
+ }
+ return res;
+ }
+
+ /**
+ * Removes the data form containing extended service discovery information
* from the information returned by this XMPP entity.
*
* Since no packet is actually sent to the server it is safe to perform this
@@ -405,17 +479,45 @@ public class ServiceDiscoveryManager {
*/
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.
+ * @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 {
- return discoverInfo(entityID, null);
+ 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.verifyDiscvoerInfoVersion(nvh.getVer(), nvh.getHash(), info))
+ EntityCapsManager.addDiscoverInfoByNode(nvh.getNodeVer(), info);
+ }
+
+ return info;
}
/**
@@ -423,8 +525,11 @@ public class ServiceDiscoveryManager {
* note attribute. Use this message only when trying to query information which is not
* directly addressable.
*
+ * @see XEP-30 Basic Protocol
+ * @see XEP-30 Info Nodes
+ *
* @param entityID the address of the XMPP entity.
- * @param node the attribute that supplements the 'jid' attribute.
+ * @param node the optional attribute that supplements the 'jid' attribute.
* @return the discovered information.
* @throws XMPPException if the operation failed for some reason.
*/
@@ -471,7 +576,7 @@ public class ServiceDiscoveryManager {
* directly addressable.
*
* @param entityID the address of the XMPP entity.
- * @param node the attribute that supplements the 'jid' attribute.
+ * @param node the optional attribute that supplements the 'jid' attribute.
* @return the discovered items.
* @throws XMPPException if the operation failed for some reason.
*/
@@ -513,8 +618,21 @@ public class ServiceDiscoveryManager {
*/
public boolean canPublishItems(String entityID) throws XMPPException {
DiscoverInfo info = discoverInfo(entityID);
- return info.containsFeature("http://jabber.org/protocol/disco#publish");
- }
+ 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
@@ -565,4 +683,26 @@ public class ServiceDiscoveryManager {
throw new XMPPException(result.getError());
}
}
-}
\ No newline at end of file
+
+ /**
+ * 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();
+ }
+}
diff --git a/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java b/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java
index d80c1ac63..f32c48ec2 100755
--- a/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java
+++ b/source/org/jivesoftware/smackx/commands/AdHocCommandManager.java
@@ -25,6 +25,7 @@ import org.jivesoftware.smack.filter.PacketFilter;
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.smack.util.StringUtils;
import org.jivesoftware.smackx.Form;
@@ -181,12 +182,16 @@ public class AdHocCommandManager {
public List getNodeIdentities() {
List answer = new ArrayList();
DiscoverInfo.Identity identity = new DiscoverInfo.Identity(
- "automation", name);
- identity.setType("command-node");
+ "automation", name, "command-node");
answer.add(identity);
return answer;
}
+ @Override
+ public List getNodePacketExtensions() {
+ return null;
+ }
+
});
}
@@ -319,6 +324,11 @@ public class AdHocCommandManager {
public List getNodeIdentities() {
return null;
}
+
+ @Override
+ public List getNodePacketExtensions() {
+ return null;
+ }
});
// The packet listener and the filter for processing some AdHoc Commands
diff --git a/source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java b/source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java
new file mode 100644
index 000000000..d5d6402d2
--- /dev/null
+++ b/source/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java
@@ -0,0 +1,713 @@
+/**
+ * Copyright 2009 Jonas Ådahl.
+ * Copyright 2011-2013 Florian Schmaus
+ *
+ * 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.entitycaps;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketInterceptor;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.filter.NotFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.filter.PacketExtensionFilter;
+import org.jivesoftware.smack.util.Base64;
+import org.jivesoftware.smack.util.Cache;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.NodeInformationProvider;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.entitycaps.cache.EntityCapsPersistentCache;
+import org.jivesoftware.smackx.entitycaps.packet.CapsExtension;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Feature;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
+import org.jivesoftware.smackx.packet.DiscoverItems.Item;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Keeps track of entity capabilities.
+ *
+ * @author Florian Schmaus
+ */
+public class EntityCapsManager {
+
+ public static final String NAMESPACE = "http://jabber.org/protocol/caps";
+ public static final String ELEMENT = "c";
+
+ private static final String ENTITY_NODE = "http://www.igniterealtime.org/projects/smack";
+ private static final Map SUPPORTED_HASHES = new HashMap();
+
+ protected static EntityCapsPersistentCache persistentCache;
+
+ private static Map instances = Collections
+ .synchronizedMap(new WeakHashMap());
+
+ /**
+ * Map of (node + '#" + hash algorithm) to DiscoverInfo data
+ */
+ protected static Map caps = new Cache(1000, -1);
+
+ /**
+ * Map of Full JID -> DiscoverInfo/null. In case of c2s connection the
+ * key is formed as user@server/resource (resource is required) In case of
+ * link-local connection the key is formed as user@host (no resource) In
+ * case of a server or component the key is formed as domain
+ */
+ protected static Map jidCaps = new Cache(10000, -1);
+
+ static {
+ Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+ public void connectionCreated(Connection connection) {
+ if (connection instanceof XMPPConnection)
+ new EntityCapsManager(connection);
+ }
+ });
+
+ try {
+ MessageDigest sha1MessageDigest = MessageDigest.getInstance("SHA-1");
+ SUPPORTED_HASHES.put("sha-1", sha1MessageDigest);
+ } catch (NoSuchAlgorithmException e) {
+ // Ignore
+ }
+ }
+
+ private WeakReference weakRefConnection;
+ private ServiceDiscoveryManager sdm;
+ private boolean entityCapsEnabled;
+ private String currentCapsVersion;
+ private boolean presenceSend = false;
+ private Queue lastLocalCapsVersions = new ConcurrentLinkedQueue();
+
+ /**
+ * Add DiscoverInfo to the database.
+ *
+ * @param nodeVer
+ * The node and verification String (e.g.
+ * "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
+ * @param info
+ * DiscoverInfo for the specified node.
+ */
+ public static void addDiscoverInfoByNode(String nodeVer, DiscoverInfo info) {
+ caps.put(nodeVer, info);
+
+ if (persistentCache != null)
+ persistentCache.addDiscoverInfoByNodePersistent(nodeVer, info);
+ }
+
+ /**
+ * Get the Node version (node#ver) of a JID. Returns a String or null if
+ * EntiyCapsManager does not have any information.
+ *
+ * @param user
+ * the user (Full JID)
+ * @return the node version (node#ver) or null
+ */
+ public static String getNodeVersionByJid(String jid) {
+ NodeVerHash nvh = jidCaps.get(jid);
+ if (nvh != null) {
+ return nvh.nodeVer;
+ } else {
+ return null;
+ }
+ }
+
+ public static NodeVerHash getNodeVerHashByJid(String jid) {
+ return jidCaps.get(jid);
+ }
+
+ /**
+ * Get the discover info given a user name. The discover info is returned if
+ * the user has a node#ver associated with it and the node#ver has a
+ * discover info associated with it.
+ *
+ * @param user
+ * user name (Full JID)
+ * @return the discovered info
+ */
+ public static DiscoverInfo getDiscoverInfoByUser(String user) {
+ NodeVerHash nvh = jidCaps.get(user);
+ if (nvh == null)
+ return null;
+
+ return getDiscoveryInfoByNodeVer(nvh.nodeVer);
+ }
+
+ /**
+ * Retrieve DiscoverInfo for a specific node.
+ *
+ * @param nodeVer
+ * The node name (e.g.
+ * "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
+ * @return The corresponding DiscoverInfo or null if none is known.
+ */
+ public static DiscoverInfo getDiscoveryInfoByNodeVer(String nodeVer) {
+ DiscoverInfo info = caps.get(nodeVer);
+ if (info != null)
+ info = new DiscoverInfo(info);
+
+ return info;
+ }
+
+ /**
+ * Set the persistent cache implementation
+ *
+ * @param cache
+ * @throws IOException
+ */
+ public static void setPersistentCache(EntityCapsPersistentCache cache) throws IOException {
+ if (persistentCache != null)
+ throw new IllegalStateException("Entity Caps Persistent Cache was already set");
+ persistentCache = cache;
+ persistentCache.replay();
+ }
+
+ /**
+ * Sets the maximum Cache size for the JID to nodeVer Cache
+ *
+ * @param maxCacheSize
+ */
+ @SuppressWarnings("rawtypes")
+ public static void setJidCapsMaxCacheSize(int maxCacheSize) {
+ ((Cache) jidCaps).setMaxCacheSize(maxCacheSize);
+ }
+
+ /**
+ * Sets the maximum Cache size for the nodeVer to DiscoverInfo Cache
+ *
+ * @param maxCacheSize
+ */
+ @SuppressWarnings("rawtypes")
+ public static void setCapsMaxCacheSize(int maxCacheSize) {
+ ((Cache) caps).setMaxCacheSize(maxCacheSize);
+ }
+
+ private EntityCapsManager(Connection connection) {
+ this.weakRefConnection = new WeakReference(connection);
+ this.sdm = ServiceDiscoveryManager.getInstanceFor(connection);
+ init();
+ }
+
+ private void init() {
+ Connection connection = weakRefConnection.get();
+ instances.put(connection, this);
+
+ connection.addConnectionListener(new ConnectionListener() {
+ public void connectionClosed() {
+ // Unregister this instance since the connection has been closed
+ presenceSend = false;
+ instances.remove(weakRefConnection.get());
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ presenceSend = false;
+ }
+
+ public void reconnectionFailed(Exception e) {
+ // ignore
+ }
+
+ public void reconnectingIn(int seconds) {
+ // ignore
+ }
+
+ public void reconnectionSuccessful() {
+ // ignore
+ }
+ });
+
+ // This calculates the local entity caps version
+ updateLocalEntityCaps();
+
+ if (SmackConfiguration.autoEnableEntityCaps())
+ enableEntityCaps();
+
+ PacketFilter packetFilter = new AndFilter(new PacketTypeFilter(Presence.class), new PacketExtensionFilter(
+ ELEMENT, NAMESPACE));
+ connection.addPacketListener(new PacketListener() {
+ // Listen for remote presence stanzas with the caps extension
+ // If we receive such a stanza, record the JID and nodeVer
+ @Override
+ public void processPacket(Packet packet) {
+ if (!entityCapsEnabled())
+ return;
+
+ CapsExtension ext = (CapsExtension) packet.getExtension(EntityCapsManager.ELEMENT,
+ EntityCapsManager.NAMESPACE);
+
+ String hash = ext.getHash().toLowerCase();
+ if (!SUPPORTED_HASHES.containsKey(hash))
+ return;
+
+ String from = packet.getFrom();
+ String node = ext.getNode();
+ String ver = ext.getVer();
+
+ jidCaps.put(from, new NodeVerHash(node, ver, hash));
+ }
+
+ }, packetFilter);
+
+ packetFilter = new AndFilter(new PacketTypeFilter(Presence.class), new NotFilter(new PacketExtensionFilter(
+ ELEMENT, NAMESPACE)));
+ connection.addPacketListener(new PacketListener() {
+ @Override
+ public void processPacket(Packet packet) {
+ // always remove the JID from the map, even if entityCaps are
+ // disabled
+ String from = packet.getFrom();
+ jidCaps.remove(from);
+ }
+ }, packetFilter);
+
+ packetFilter = new PacketTypeFilter(Presence.class);
+ connection.addPacketSendingListener(new PacketListener() {
+ @Override
+ public void processPacket(Packet packet) {
+ presenceSend = true;
+ }
+ }, packetFilter);
+
+ // Intercept presence packages and add caps data when intended.
+ // XEP-0115 specifies that a client SHOULD include entity capabilities
+ // with every presence notification it sends.
+ PacketFilter capsPacketFilter = new PacketTypeFilter(Presence.class);
+ PacketInterceptor packetInterceptor = new PacketInterceptor() {
+ public void interceptPacket(Packet packet) {
+ if (!entityCapsEnabled)
+ return;
+
+ CapsExtension caps = new CapsExtension(ENTITY_NODE, getCapsVersion(), "sha-1");
+ packet.addExtension(caps);
+ }
+ };
+ connection.addPacketInterceptor(packetInterceptor, capsPacketFilter);
+ // It's important to do this as last action. Since it changes the
+ // behavior of the SDM in some ways
+ sdm.setEntityCapsManager(this);
+ }
+
+ public static synchronized EntityCapsManager getInstanceFor(Connection connection) {
+ // For testing purposed forbid EntityCaps for non XMPPConnections
+ // it may work on BOSH connections too
+ if (!(connection instanceof XMPPConnection))
+ return null;
+
+ if (SUPPORTED_HASHES.size() <= 0)
+ return null;
+
+ EntityCapsManager entityCapsManager = instances.get(connection);
+
+ if (entityCapsManager == null) {
+ entityCapsManager = new EntityCapsManager(connection);
+ }
+
+ return entityCapsManager;
+ }
+
+ public void enableEntityCaps() {
+ // Add Entity Capabilities (XEP-0115) feature node.
+ sdm.addFeature(NAMESPACE);
+ updateLocalEntityCaps();
+ entityCapsEnabled = true;
+ }
+
+ public void disableEntityCaps() {
+ entityCapsEnabled = false;
+ sdm.removeFeature(NAMESPACE);
+ }
+
+ public boolean entityCapsEnabled() {
+ return entityCapsEnabled;
+ }
+
+ /**
+ * Remove a record telling what entity caps node a user has.
+ *
+ * @param user
+ * the user (Full JID)
+ */
+ public void removeUserCapsNode(String user) {
+ jidCaps.remove(user);
+ }
+
+ /**
+ * Get our own caps version. The version depends on the enabled features. A
+ * caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI='
+ *
+ * @return our own caps version
+ */
+ public String getCapsVersion() {
+ return currentCapsVersion;
+ }
+
+ /**
+ * Returns the local entity's NodeVer (e.g.
+ * "http://www.igniterealtime.org/projects/smack/#66/0NaeaBKkwk85efJTGmU47vXI=
+ * )
+ *
+ * @return
+ */
+ public String getLocalNodeVer() {
+ return ENTITY_NODE + '#' + getCapsVersion();
+ }
+
+ /**
+ * Returns true if Entity Caps are supported by a given JID
+ *
+ * @param jid
+ * @return
+ */
+ public boolean areEntityCapsSupported(String jid) {
+ if (jid == null)
+ return false;
+
+ try {
+ DiscoverInfo result = sdm.discoverInfo(jid);
+ return result.containsFeature(NAMESPACE);
+ } catch (XMPPException e) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns true if Entity Caps are supported by the local service/server
+ *
+ * @return
+ */
+ public boolean areEntityCapsSupportedByServer() {
+ return areEntityCapsSupported(weakRefConnection.get().getServiceName());
+ }
+
+ /**
+ * Updates the local user Entity Caps information with the data provided
+ *
+ * If we are connected and there was already a presence send, another
+ * presence is send to inform others about your new Entity Caps node string.
+ *
+ * @param discoverInfo
+ * the local users discover info (mostly the service discovery
+ * features)
+ * @param identityType
+ * the local users identity type
+ * @param identityName
+ * the local users identity name
+ * @param extendedInfo
+ * the local users extended info
+ */
+ public void updateLocalEntityCaps() {
+ Connection connection = weakRefConnection.get();
+
+ DiscoverInfo discoverInfo = new DiscoverInfo();
+ discoverInfo.setType(IQ.Type.RESULT);
+ discoverInfo.setNode(getLocalNodeVer());
+ if (connection != null)
+ discoverInfo.setFrom(connection.getUser());
+ sdm.addDiscoverInfoTo(discoverInfo);
+
+ currentCapsVersion = generateVerificationString(discoverInfo, "sha-1");
+ addDiscoverInfoByNode(ENTITY_NODE + '#' + currentCapsVersion, discoverInfo);
+ if (lastLocalCapsVersions.size() > 10) {
+ String oldCapsVersion = lastLocalCapsVersions.poll();
+ sdm.removeNodeInformationProvider(ENTITY_NODE + '#' + oldCapsVersion);
+ }
+ lastLocalCapsVersions.add(currentCapsVersion);
+
+ caps.put(currentCapsVersion, discoverInfo);
+ if (connection != null)
+ jidCaps.put(connection.getUser(), new NodeVerHash(ENTITY_NODE, currentCapsVersion, "sha-1"));
+
+ sdm.setNodeInformationProvider(ENTITY_NODE + '#' + currentCapsVersion, new NodeInformationProvider() {
+ List features = sdm.getFeaturesList();
+ List identities = new LinkedList(ServiceDiscoveryManager.getIdentities());
+ List packetExtensions = sdm.getExtendedInfoAsList();
+
+ @Override
+ public List getNodeItems() {
+ return null;
+ }
+
+ @Override
+ public List getNodeFeatures() {
+ return features;
+ }
+
+ @Override
+ public List getNodeIdentities() {
+ return identities;
+ }
+
+ @Override
+ public List getNodePacketExtensions() {
+ return packetExtensions;
+ }
+ });
+
+ // Send an empty presence, and let the packet intercepter
+ // add a node to it.
+ // See http://xmpp.org/extensions/xep-0115.html#advertise
+ // We only send a presence packet if there was already one send
+ // to respect ConnectionConfiguration.isSendPresence()
+ if (connection != null && connection.isAuthenticated() && presenceSend) {
+ Presence presence = new Presence(Presence.Type.available);
+ connection.sendPacket(presence);
+ }
+ }
+
+ /**
+ * Verify DisoverInfo and Caps Node as defined in XEP-0115 5.4 Processing
+ * Method
+ *
+ * @see XEP-0115
+ * 5.4 Processing Method
+ *
+ * @param capsNode
+ * the caps node (i.e. node#ver)
+ * @param info
+ * @return true if it's valid and should be cache, false if not
+ */
+ public static boolean verifyDiscvoerInfoVersion(String ver, String hash, DiscoverInfo info) {
+ // step 3.3 check for duplicate identities
+ if (info.containsDuplicateIdentities())
+ return false;
+
+ // step 3.4 check for duplicate features
+ if (info.containsDuplicateFeatures())
+ return false;
+
+ // step 3.5 check for well-formed packet extensions
+ if (verifyPacketExtensions(info))
+ return false;
+
+ String calculatedVer = generateVerificationString(info, hash);
+
+ if (!ver.equals(calculatedVer))
+ return false;
+
+ return true;
+ }
+
+ /**
+ *
+ * @param info
+ * @return true if the packet extensions is ill-formed
+ */
+ protected static boolean verifyPacketExtensions(DiscoverInfo info) {
+ List foundFormTypes = new LinkedList();
+ for (Iterator i = info.getExtensions().iterator(); i.hasNext();) {
+ PacketExtension pe = i.next();
+ if (pe.getNamespace().equals(Form.NAMESPACE)) {
+ DataForm df = (DataForm) pe;
+ for (Iterator it = df.getFields(); it.hasNext();) {
+ FormField f = it.next();
+ if (f.getVariable().equals("FORM_TYPE")) {
+ for (FormField fft : foundFormTypes) {
+ if (f.equals(fft))
+ return true;
+ }
+ foundFormTypes.add(f);
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Generates a XEP-115 Verification String
+ *
+ * @see XEP-115
+ * Verification String
+ *
+ * @param discoverInfo
+ * @param hash
+ * the used hash function
+ * @return The generated verification String or null if the hash is not
+ * supported
+ */
+ protected static String generateVerificationString(DiscoverInfo discoverInfo, String hash) {
+ MessageDigest md = SUPPORTED_HASHES.get(hash.toLowerCase());
+ if (md == null)
+ return null;
+
+ DataForm extendedInfo = (DataForm) discoverInfo.getExtension(Form.ELEMENT, Form.NAMESPACE);
+
+ // 1. Initialize an empty string S ('sb' in this method).
+ StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't
+ // need thread-safe StringBuffer
+
+ // 2. Sort the service discovery identities by category and then by
+ // type and then by xml:lang
+ // (if it exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/'
+ // [NAME]. Note that each slash is included even if the LANG or
+ // NAME is not included (in accordance with XEP-0030, the category and
+ // type MUST be included.
+ SortedSet sortedIdentities = new TreeSet();
+ ;
+ for (Iterator it = discoverInfo.getIdentities(); it.hasNext();)
+ sortedIdentities.add(it.next());
+
+ // 3. For each identity, append the 'category/type/lang/name' to S,
+ // followed by the '<' character.
+ for (Iterator it = sortedIdentities.iterator(); it.hasNext();) {
+ DiscoverInfo.Identity identity = it.next();
+ sb.append(identity.getCategory());
+ sb.append("/");
+ sb.append(identity.getType());
+ sb.append("/");
+ sb.append(identity.getLanguage() == null ? "" : identity.getLanguage());
+ sb.append("/");
+ sb.append(identity.getName() == null ? "" : identity.getName());
+ sb.append("<");
+ }
+
+ // 4. Sort the supported service discovery features.
+ SortedSet features = new TreeSet();
+ for (Iterator it = discoverInfo.getFeatures(); it.hasNext();)
+ features.add(it.next().getVar());
+
+ // 5. For each feature, append the feature to S, followed by the '<'
+ // character
+ for (String f : features) {
+ sb.append(f);
+ sb.append("<");
+ }
+
+ // only use the data form for calculation is it has a hidden FORM_TYPE
+ // field
+ // see XEP-0115 5.4 step 3.6
+ if (extendedInfo != null && extendedInfo.hasHiddenFromTypeField()) {
+ synchronized (extendedInfo) {
+ // 6. If the service discovery information response includes
+ // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e.,
+ // by the XML character data of the element).
+ SortedSet fs = new TreeSet(new Comparator() {
+ public int compare(FormField f1, FormField f2) {
+ return f1.getVariable().compareTo(f2.getVariable());
+ }
+ });
+
+ FormField ft = null;
+
+ for (Iterator i = extendedInfo.getFields(); i.hasNext();) {
+ FormField f = i.next();
+ if (!f.getVariable().equals("FORM_TYPE")) {
+ fs.add(f);
+ } else {
+ ft = f;
+ }
+ }
+
+ // Add FORM_TYPE values
+ if (ft != null) {
+ formFieldValuesToCaps(ft.getValues(), sb);
+ }
+
+ // 7. 3. For each field other than FORM_TYPE:
+ // 1. Append the value of the "var" attribute, followed by the
+ // '<' character.
+ // 2. Sort values by the XML character data of the
+ // element.
+ // 3. For each element, append the XML character data,
+ // followed by the '<' character.
+ for (FormField f : fs) {
+ sb.append(f.getVariable());
+ sb.append("<");
+ formFieldValuesToCaps(f.getValues(), sb);
+ }
+ }
+ }
+ // 8. Ensure that S is encoded according to the UTF-8 encoding (RFC
+ // 3269).
+ // 9. Compute the verification string by hashing S using the algorithm
+ // specified in the 'hash' attribute (e.g., SHA-1 as defined in RFC
+ // 3174).
+ // The hashed data MUST be generated with binary output and
+ // encoded using Base64 as specified in Section 4 of RFC 4648
+ // (note: the Base64 output MUST NOT include whitespace and MUST set
+ // padding bits to zero).
+ byte[] digest = md.digest(sb.toString().getBytes());
+ return Base64.encodeBytes(digest);
+ }
+
+ private static void formFieldValuesToCaps(Iterator i, StringBuilder sb) {
+ SortedSet fvs = new TreeSet();
+ while (i.hasNext()) {
+ fvs.add(i.next());
+ }
+ for (String fv : fvs) {
+ sb.append(fv);
+ sb.append("<");
+ }
+ }
+
+ public static class NodeVerHash {
+ private String node;
+ private String hash;
+ private String ver;
+ private String nodeVer;
+
+ NodeVerHash(String node, String ver, String hash) {
+ this.node = node;
+ this.ver = ver;
+ this.hash = hash;
+ nodeVer = node + "#" + ver;
+ }
+
+ public String getNodeVer() {
+ return nodeVer;
+ }
+
+ public String getNode() {
+ return node;
+ }
+
+ public String getHash() {
+ return hash;
+ }
+
+ public String getVer() {
+ return ver;
+ }
+ }
+}
diff --git a/source/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java b/source/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java
new file mode 100644
index 000000000..4247e7b1b
--- /dev/null
+++ b/source/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java
@@ -0,0 +1,38 @@
+/**
+ * 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.entitycaps.cache;
+
+import java.io.IOException;
+
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+
+public interface EntityCapsPersistentCache {
+ /**
+ * Add an DiscoverInfo to the persistent Cache
+ *
+ * @param node
+ * @param info
+ */
+ abstract void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info);
+
+ /**
+ * Replay the Caches data into EntityCapsManager
+ */
+ abstract void replay() throws IOException;
+
+ /**
+ * Empty the Cache
+ */
+ abstract void emptyCache();
+}
diff --git a/source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java b/source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java
new file mode 100644
index 000000000..329e4dce6
--- /dev/null
+++ b/source/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java
@@ -0,0 +1,193 @@
+/**
+ * Copyright 2011 Florian Schmaus
+ *
+ * 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.entitycaps.cache;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.Base64Encoder;
+import org.jivesoftware.smack.util.StringEncoder;
+import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.provider.DiscoverInfoProvider;
+import org.xmlpull.mxp1.MXParser;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Simple implementation of an EntityCapsPersistentCache that uses a directory
+ * to store the Caps information for every known node. Every node is represented
+ * by an file.
+ *
+ * @author Florian Schmaus
+ *
+ */
+public class SimpleDirectoryPersistentCache implements EntityCapsPersistentCache {
+
+ private File cacheDir;
+ private StringEncoder stringEncoder;
+
+ /**
+ * Creates a new SimpleDirectoryPersistentCache Object. Make sure that the
+ * cacheDir exists and that it's an directory.
+ *
+ * If your cacheDir is case insensitive then make sure to set the
+ * StringEncoder to Base32.
+ *
+ * @param cacheDir
+ */
+ public SimpleDirectoryPersistentCache(File cacheDir) {
+ this(cacheDir, Base64Encoder.getInstance());
+ }
+
+ /**
+ * Creates a new SimpleDirectoryPersistentCache Object. Make sure that the
+ * cacheDir exists and that it's an directory.
+ *
+ * If your cacheDir is case insensitive then make sure to set the
+ * StringEncoder to Base32.
+ *
+ * @param cacheDir
+ * @param stringEncoder
+ */
+ public SimpleDirectoryPersistentCache(File cacheDir, StringEncoder stringEncoder) {
+ if (!cacheDir.exists())
+ throw new IllegalStateException("Cache directory \"" + cacheDir + "\" does not exist");
+ if (!cacheDir.isDirectory())
+ throw new IllegalStateException("Cache directory \"" + cacheDir + "\" is not a directory");
+
+ this.cacheDir = cacheDir;
+ this.stringEncoder = stringEncoder;
+ }
+
+ @Override
+ public void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info) {
+ String filename = stringEncoder.encode(node);
+ File nodeFile = new File(cacheDir, filename);
+
+ try {
+ if (nodeFile.createNewFile())
+ writeInfoToFile(nodeFile, info);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void replay() throws IOException {
+ File[] files = cacheDir.listFiles();
+ for (File f : files) {
+ String node = stringEncoder.decode(f.getName());
+ DiscoverInfo info = restoreInfoFromFile(f);
+ if (info == null)
+ continue;
+
+ EntityCapsManager.addDiscoverInfoByNode(node, info);
+ }
+ }
+
+ public void emptyCache() {
+ File[] files = cacheDir.listFiles();
+ for (File f : files) {
+ f.delete();
+ }
+ }
+
+ /**
+ * Writes the DiscoverInfo packet to an file
+ *
+ * @param file
+ * @param info
+ * @throws IOException
+ */
+ private static void writeInfoToFile(File file, DiscoverInfo info) throws IOException {
+ DataOutputStream dos = new DataOutputStream(new FileOutputStream(file));
+ try {
+ dos.writeUTF(info.toXML());
+ } finally {
+ dos.close();
+ }
+ }
+
+ /**
+ * Tries to restore an DiscoverInfo packet from a file.
+ *
+ * @param file
+ * @return
+ * @throws IOException
+ */
+ private static DiscoverInfo restoreInfoFromFile(File file) throws IOException {
+ DataInputStream dis = new DataInputStream(new FileInputStream(file));
+ String fileContent = null;
+ String id;
+ String from;
+ String to;
+
+ try {
+ fileContent = dis.readUTF();
+ } finally {
+ dis.close();
+ }
+ if (fileContent == null)
+ return null;
+
+ Reader reader = new StringReader(fileContent);
+ XmlPullParser parser;
+ try {
+ parser = new MXParser();
+ parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+ parser.setInput(reader);
+ } catch (XmlPullParserException xppe) {
+ xppe.printStackTrace();
+ return null;
+ }
+
+ DiscoverInfo iqPacket;
+ IQProvider provider = new DiscoverInfoProvider();
+
+ // Parse the IQ, we only need the id
+ try {
+ parser.next();
+ id = parser.getAttributeValue("", "id");
+ from = parser.getAttributeValue("", "from");
+ to = parser.getAttributeValue("", "to");
+ parser.next();
+ } catch (XmlPullParserException e1) {
+ return null;
+ }
+
+ try {
+ iqPacket = (DiscoverInfo) provider.parseIQ(parser);
+ } catch (Exception e) {
+ return null;
+ }
+
+ iqPacket.setPacketID(id);
+ iqPacket.setFrom(from);
+ iqPacket.setTo(to);
+ iqPacket.setType(IQ.Type.RESULT);
+ return iqPacket;
+ }
+}
diff --git a/source/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java b/source/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java
new file mode 100644
index 000000000..a87c86c96
--- /dev/null
+++ b/source/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright 2009 Jonas Ådahl.
+ * Copyright 2011-2013 Florian Schmaus
+ *
+ * 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.entitycaps.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
+
+public class CapsExtension implements PacketExtension {
+
+ private String node, ver, hash;
+
+ public CapsExtension() {
+ }
+
+ public CapsExtension(String node, String version, String hash) {
+ this.node = node;
+ this.ver = version;
+ this.hash = hash;
+ }
+
+ public String getElementName() {
+ return EntityCapsManager.ELEMENT;
+ }
+
+ public String getNamespace() {
+ return EntityCapsManager.NAMESPACE;
+ }
+
+ public String getNode() {
+ return node;
+ }
+
+ public void setNode(String node) {
+ this.node = node;
+ }
+
+ public String getVer() {
+ return ver;
+ }
+
+ public void setVer(String ver) {
+ this.ver = ver;
+ }
+
+ public String getHash() {
+ return hash;
+ }
+
+ public void setHash(String hash) {
+ this.hash = hash;
+ }
+
+ /*
+ *
+ *
+ */
+ public String toXML() {
+ String xml = "<" + EntityCapsManager.ELEMENT + " xmlns=\"" + EntityCapsManager.NAMESPACE + "\" " +
+ "hash=\"" + hash + "\" " +
+ "node=\"" + node + "\" " +
+ "ver=\"" + ver + "\"/>";
+
+ return xml;
+ }
+}
diff --git a/source/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java b/source/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java
new file mode 100644
index 000000000..4328d21b3
--- /dev/null
+++ b/source/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2009 Jonas Ådahl.
+ * Copyright 2011-2013 Florian Schmaus
+ *
+ * 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.entitycaps.provider;
+
+import java.io.IOException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
+import org.jivesoftware.smackx.entitycaps.packet.CapsExtension;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public class CapsExtensionProvider implements PacketExtensionProvider {
+
+ public PacketExtension parseExtension(XmlPullParser parser) throws XmlPullParserException, IOException,
+ XMPPException {
+ String hash = null;
+ String version = null;
+ String node = null;
+ if (parser.getEventType() == XmlPullParser.START_TAG
+ && parser.getName().equalsIgnoreCase(EntityCapsManager.ELEMENT)) {
+ hash = parser.getAttributeValue(null, "hash");
+ version = parser.getAttributeValue(null, "ver");
+ node = parser.getAttributeValue(null, "node");
+ } else {
+ throw new XMPPException("Malformed Caps element");
+ }
+
+ parser.next();
+
+ if (!(parser.getEventType() == XmlPullParser.END_TAG
+ && parser.getName().equalsIgnoreCase(EntityCapsManager.ELEMENT))) {
+ throw new XMPPException("Malformed nested Caps element");
+ }
+
+ if (hash != null && version != null && node != null) {
+ return new CapsExtension(node, version, hash);
+ } else {
+ throw new XMPPException("Caps elment with missing attributes");
+ }
+ }
+}
diff --git a/source/org/jivesoftware/smackx/muc/MultiUserChat.java b/source/org/jivesoftware/smackx/muc/MultiUserChat.java
index 2caa970a1..e0368028d 100644
--- a/source/org/jivesoftware/smackx/muc/MultiUserChat.java
+++ b/source/org/jivesoftware/smackx/muc/MultiUserChat.java
@@ -52,6 +52,7 @@ import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Registration;
import org.jivesoftware.smackx.Form;
@@ -133,6 +134,11 @@ public class MultiUserChat {
public List getNodeIdentities() {
return null;
}
+
+ @Override
+ public List getNodePacketExtensions() {
+ return null;
+ }
});
}
});
diff --git a/source/org/jivesoftware/smackx/packet/DataForm.java b/source/org/jivesoftware/smackx/packet/DataForm.java
index 8fe43070f..4bc1f6994 100644
--- a/source/org/jivesoftware/smackx/packet/DataForm.java
+++ b/source/org/jivesoftware/smackx/packet/DataForm.java
@@ -21,6 +21,7 @@
package org.jivesoftware.smackx.packet;
import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.FormField;
import java.util.ArrayList;
@@ -123,11 +124,11 @@ public class DataForm implements PacketExtension {
}
public String getElementName() {
- return "x";
+ return Form.ELEMENT;
}
public String getNamespace() {
- return "jabber:x:data";
+ return Form.NAMESPACE;
}
/**
@@ -195,6 +196,21 @@ public class DataForm implements PacketExtension {
}
}
+ /**
+ * Returns true if this DataForm has at least one FORM_TYPE field which is
+ * hidden. This method is used for sanity checks.
+ *
+ * @return
+ */
+ public boolean hasHiddenFromTypeField() {
+ boolean found = false;
+ for (FormField f : fields) {
+ if (f.getVariable().equals("FORM_TYPE") && f.getType() != null && f.getType().equals("hidden"))
+ found = true;
+ }
+ return found;
+ }
+
public String toXML() {
StringBuilder buf = new StringBuilder();
buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
diff --git a/source/org/jivesoftware/smackx/packet/DiscoverInfo.java b/source/org/jivesoftware/smackx/packet/DiscoverInfo.java
index 4f4597d67..e2219032d 100644
--- a/source/org/jivesoftware/smackx/packet/DiscoverInfo.java
+++ b/source/org/jivesoftware/smackx/packet/DiscoverInfo.java
@@ -23,8 +23,10 @@ package org.jivesoftware.smackx.packet;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.util.StringUtils;
+import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@@ -45,6 +47,36 @@ public class DiscoverInfo extends IQ {
private final List identities = new CopyOnWriteArrayList();
private String node;
+ public DiscoverInfo() {
+ super();
+ }
+
+ /**
+ * Copy constructor
+ *
+ * @param d
+ */
+ public DiscoverInfo(DiscoverInfo d) {
+ super(d);
+
+ // Set node
+ setNode(d.getNode());
+
+ // Copy features
+ synchronized (d.features) {
+ for (Feature f : d.features) {
+ addFeature(f);
+ }
+ }
+
+ // Copy identities
+ synchronized (d.identities) {
+ for (Identity i : d.identities) {
+ addIdentity(i);
+ }
+ }
+ }
+
/**
* Adds a new feature to the discovered information.
*
@@ -54,6 +86,18 @@ public class DiscoverInfo extends IQ {
addFeature(new Feature(feature));
}
+ /**
+ * Adds a collection of features to the packet. Does noting if featuresToAdd is null.
+ *
+ * @param featuresToAdd
+ */
+ public void addFeatures(Collection featuresToAdd) {
+ if (featuresToAdd == null) return;
+ for (String feature : featuresToAdd) {
+ addFeature(feature);
+ }
+ }
+
private void addFeature(Feature feature) {
synchronized (features) {
features.add(feature);
@@ -82,6 +126,18 @@ public class DiscoverInfo extends IQ {
}
}
+ /**
+ * Adds identities to the DiscoverInfo stanza
+ *
+ * @param identitiesToAdd
+ */
+ public void addIdentities(Collection identitiesToAdd) {
+ if (identitiesToAdd == null) return;
+ synchronized (identities) {
+ identities.addAll(identitiesToAdd);
+ }
+ }
+
/**
* Returns the discovered identities of an XMPP entity.
*
@@ -158,6 +214,40 @@ public class DiscoverInfo extends IQ {
return buf.toString();
}
+ /**
+ * Test if a DiscoverInfo response contains duplicate identities.
+ *
+ * @return true if duplicate identities where found, otherwise false
+ */
+ public boolean containsDuplicateIdentities() {
+ List checkedIdentities = new LinkedList();
+ for (Identity i : identities) {
+ for (Identity i2 : checkedIdentities) {
+ if (i.equals(i2))
+ return true;
+ }
+ checkedIdentities.add(i);
+ }
+ return false;
+ }
+
+ /**
+ * Test if a DiscoverInfo response contains duplicate features.
+ *
+ * @return true if duplicate identities where found, otherwise false
+ */
+ public boolean containsDuplicateFeatures() {
+ List checkedFeatures = new LinkedList();
+ for (Feature f : features) {
+ for (Feature f2 : checkedFeatures) {
+ if (f.equals(f2))
+ return true;
+ }
+ checkedFeatures.add(f);
+ }
+ return false;
+ }
+
/**
* Represents the identity of a given XMPP entity. An entity may have many identities but all
* the identities SHOULD have the same name.
@@ -167,21 +257,26 @@ public class DiscoverInfo extends IQ {
* attributes.
*
*/
- public static class Identity {
+ public static class Identity implements Comparable