diff --git a/build/resources/META-INF/smack-config.xml b/build/resources/META-INF/smack-config.xml index c4e99936a..6c4b891e2 100644 --- a/build/resources/META-INF/smack-config.xml +++ b/build/resources/META-INF/smack-config.xml @@ -30,8 +30,11 @@ 10000 - + 1800 + + false + diff --git a/build/resources/META-INF/smack.providers b/build/resources/META-INF/smack.providers index 0de71aa04..f1a7c1f91 100644 --- a/build/resources/META-INF/smack.providers +++ b/build/resources/META-INF/smack.providers @@ -678,4 +678,11 @@ urn:xmpp:receipts org.jivesoftware.smackx.receipts.DeliveryReceiptRequest$Provider + + + + c + http://jabber.org/protocol/caps + org.jivesoftware.smackx.entitycaps.provider.CapsExtensionProvider + diff --git a/source/org/jivesoftware/smack/Connection.java b/source/org/jivesoftware/smack/Connection.java index d041067a4..9a213dbc8 100644 --- a/source/org/jivesoftware/smack/Connection.java +++ b/source/org/jivesoftware/smack/Connection.java @@ -204,6 +204,11 @@ public abstract class Connection { */ protected final ConnectionConfiguration config; + /** + * Holds the Caps Node information for the used XMPP service (i.e. the XMPP server) + */ + private String serviceCapsNode; + protected XMPPInputOutputStream compressionHandler; /** @@ -795,7 +800,29 @@ public abstract class Connection { } } + /** + * Set the servers Entity Caps node + * + * Connection holds this information in order to avoid a dependency to + * smackx where EntityCapsManager lives from smack. + * + * @param node + */ + protected void setServiceCapsNode(String node) { + serviceCapsNode = node; + } + /** + * Retrieve the servers Entity Caps node + * + * Connection holds this information in order to avoid a dependency to + * smackx where EntityCapsManager lives from smack. + * + * @return + */ + public String getServiceCapsNode() { + return serviceCapsNode; + } /** * A wrapper class to associate a packet filter with a listener. diff --git a/source/org/jivesoftware/smack/PacketReader.java b/source/org/jivesoftware/smack/PacketReader.java index 616a18dbe..590dfd951 100644 --- a/source/org/jivesoftware/smack/PacketReader.java +++ b/source/org/jivesoftware/smack/PacketReader.java @@ -393,6 +393,19 @@ class PacketReader { // The server requires the client to bind a resource to the stream connection.getSASLAuthentication().bindingRequired(); } + // Set the entity caps node for the server if one is send + // See http://xmpp.org/extensions/xep-0115.html#stream + else if (parser.getName().equals("c")) { + String node = parser.getAttributeValue(null, "node"); + String ver = parser.getAttributeValue(null, "ver"); + if (ver != null && node != null) { + String capsNode = node + "#" + ver; + // In order to avoid a dependency from smack to smackx + // we have to set the services caps node in the connection + // and not directly in the EntityCapsManager + connection.setServiceCapsNode(capsNode); + } + } else if (parser.getName().equals("session")) { // The server supports sessions connection.getSASLAuthentication().sessionsSupported(); diff --git a/source/org/jivesoftware/smack/SmackConfiguration.java b/source/org/jivesoftware/smack/SmackConfiguration.java index 83f8d22f3..80f1906af 100644 --- a/source/org/jivesoftware/smack/SmackConfiguration.java +++ b/source/org/jivesoftware/smack/SmackConfiguration.java @@ -63,6 +63,11 @@ public final class SmackConfiguration { */ private static int defaultPingInterval = 1800; // 30 min (30*60) + /** + * This automatically enables EntityCaps for new connections if it is set to true + */ + private static boolean autoEnableEntityCaps = false; + private SmackConfiguration() { } @@ -115,6 +120,9 @@ public final class SmackConfiguration { else if (parser.getName().equals("defaultPingInterval")) { defaultPingInterval = parseIntProperty(parser, defaultPingInterval); } + else if (parser.getName().equals("autoEnableEntityCaps")) { + autoEnableEntityCaps = Boolean.parseBoolean(parser.nextText()); + } } eventType = parser.next(); } @@ -329,6 +337,23 @@ public final class SmackConfiguration { SmackConfiguration.defaultPingInterval = defaultPingInterval; } + /** + * Check if Entity Caps are enabled as default for every new connection + * @return + */ + public static boolean autoEnableEntityCaps() { + return autoEnableEntityCaps; + } + + /** + * Set if Entity Caps are enabled or disabled for every new connection + * + * @param true if Entity Caps should be auto enabled, false if not + */ + public static void setAutoEnableEntityCaps(boolean b) { + autoEnableEntityCaps = b; + } + private static void parseClassToLoad(XmlPullParser parser) throws Exception { String className = parser.nextText(); // Attempt to load the class so that the class can get initialized diff --git a/source/org/jivesoftware/smack/packet/IQ.java b/source/org/jivesoftware/smack/packet/IQ.java index 8b844674c..8e1f7d4ab 100644 --- a/source/org/jivesoftware/smack/packet/IQ.java +++ b/source/org/jivesoftware/smack/packet/IQ.java @@ -43,6 +43,14 @@ public abstract class IQ extends Packet { private Type type = Type.GET; + public IQ() { + super(); + } + + public IQ(IQ iq) { + super(iq); + type = iq.getType(); + } /** * Returns the type of the IQ packet. * diff --git a/source/org/jivesoftware/smack/packet/Packet.java b/source/org/jivesoftware/smack/packet/Packet.java index 883462b1b..041d8c892 100644 --- a/source/org/jivesoftware/smack/packet/Packet.java +++ b/source/org/jivesoftware/smack/packet/Packet.java @@ -90,6 +90,22 @@ public abstract class Packet { private final Map properties = new HashMap(); private XMPPError error = null; + public Packet() { + } + + public Packet(Packet p) { + packetID = p.getPacketID(); + to = p.getTo(); + from = p.getFrom(); + xmlns = p.xmlns; + error = p.error; + + // Copy extensions + for (PacketExtension pe : p.getExtensions()) { + addExtension(pe); + } + } + /** * Returns the unique ID of the packet. The returned value could be null when * ID_NOT_AVAILABLE was set as the packet's id. @@ -247,14 +263,25 @@ public abstract class Packet { } /** - * Adds a packet extension to the packet. + * Adds a packet extension to the packet. Does nothing if extension is null. * * @param extension a packet extension. */ public void addExtension(PacketExtension extension) { + if (extension == null) return; packetExtensions.add(extension); } + /** + * Adds a collection of packet extensions to the packet. Does nothing if extensions is null. + * + * @param extensions a collection of packet extensions + */ + public void addExtensions(Collection extensions) { + if (extensions == null) return; + packetExtensions.addAll(extensions); + } + /** * Removes a packet extension from the packet. * @@ -266,7 +293,7 @@ public abstract class Packet { /** * Returns the packet property with the specified name or null if the - * property doesn't exist. Property values that were orginally primitives will + * property doesn't exist. Property values that were originally primitives will * be returned as their object equivalent. For example, an int property will be * returned as an Integer, a double as a Double, etc. * @@ -456,4 +483,4 @@ public abstract class Packet { result = 31 * result + (error != null ? error.hashCode() : 0); return result; } -} \ No newline at end of file +} diff --git a/source/org/jivesoftware/smack/util/Base32Encoder.java b/source/org/jivesoftware/smack/util/Base32Encoder.java new file mode 100644 index 000000000..c7cc1d028 --- /dev/null +++ b/source/org/jivesoftware/smack/util/Base32Encoder.java @@ -0,0 +1,187 @@ +/** + * 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.smack.util; + + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Base32 string encoding is useful for when filenames case-insensitive filesystems are encoded. + * Base32 representation takes roughly 20% more space then Base64. + * + * @author Florian Schmaus + * Based on code by Brian Wellington (bwelling@xbill.org) + * @see Base32 Wikipedia entry + * + */ +public class Base32Encoder implements StringEncoder { + + private static Base32Encoder instance; + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ2345678"; + + private Base32Encoder() { + // Use getInstance() + } + + public static Base32Encoder getInstance() { + if (instance == null) { + instance = new Base32Encoder(); + } + return instance; + } + + @Override + public String decode(String str) { + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + byte[] raw = str.getBytes(); + for (int i = 0; i < raw.length; i++) { + char c = (char) raw[i]; + if (!Character.isWhitespace(c)) { + c = Character.toUpperCase(c); + bs.write((byte) c); + } + } + + while (bs.size() % 8 != 0) + bs.write('8'); + + byte[] in = bs.toByteArray(); + + bs.reset(); + DataOutputStream ds = new DataOutputStream(bs); + + for (int i = 0; i < in.length / 8; i++) { + short[] s = new short[8]; + int[] t = new int[5]; + + int padlen = 8; + for (int j = 0; j < 8; j++) { + char c = (char) in[i * 8 + j]; + if (c == '8') + break; + s[j] = (short) ALPHABET.indexOf(in[i * 8 + j]); + if (s[j] < 0) + return null; + padlen--; + } + int blocklen = paddingToLen(padlen); + if (blocklen < 0) + return null; + + // all 5 bits of 1st, high 3 (of 5) of 2nd + t[0] = (s[0] << 3) | s[1] >> 2; + // lower 2 of 2nd, all 5 of 3rd, high 1 of 4th + t[1] = ((s[1] & 0x03) << 6) | (s[2] << 1) | (s[3] >> 4); + // lower 4 of 4th, high 4 of 5th + t[2] = ((s[3] & 0x0F) << 4) | ((s[4] >> 1) & 0x0F); + // lower 1 of 5th, all 5 of 6th, high 2 of 7th + t[3] = (s[4] << 7) | (s[5] << 2) | (s[6] >> 3); + // lower 3 of 7th, all of 8th + t[4] = ((s[6] & 0x07) << 5) | s[7]; + + try { + for (int j = 0; j < blocklen; j++) + ds.writeByte((byte) (t[j] & 0xFF)); + } catch (IOException e) { + } + } + + return new String(bs.toByteArray()); + } + + @Override + public String encode(String str) { + byte[] b = str.getBytes(); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + for (int i = 0; i < (b.length + 4) / 5; i++) { + short s[] = new short[5]; + int t[] = new int[8]; + + int blocklen = 5; + for (int j = 0; j < 5; j++) { + if ((i * 5 + j) < b.length) + s[j] = (short) (b[i * 5 + j] & 0xFF); + else { + s[j] = 0; + blocklen--; + } + } + int padlen = lenToPadding(blocklen); + + // convert the 5 byte block into 8 characters (values 0-31). + + // upper 5 bits from first byte + t[0] = (byte) ((s[0] >> 3) & 0x1F); + // lower 3 bits from 1st byte, upper 2 bits from 2nd. + t[1] = (byte) (((s[0] & 0x07) << 2) | ((s[1] >> 6) & 0x03)); + // bits 5-1 from 2nd. + t[2] = (byte) ((s[1] >> 1) & 0x1F); + // lower 1 bit from 2nd, upper 4 from 3rd + t[3] = (byte) (((s[1] & 0x01) << 4) | ((s[2] >> 4) & 0x0F)); + // lower 4 from 3rd, upper 1 from 4th. + t[4] = (byte) (((s[2] & 0x0F) << 1) | ((s[3] >> 7) & 0x01)); + // bits 6-2 from 4th + t[5] = (byte) ((s[3] >> 2) & 0x1F); + // lower 2 from 4th, upper 3 from 5th; + t[6] = (byte) (((s[3] & 0x03) << 3) | ((s[4] >> 5) & 0x07)); + // lower 5 from 5th; + t[7] = (byte) (s[4] & 0x1F); + + // write out the actual characters. + for (int j = 0; j < t.length - padlen; j++) { + char c = ALPHABET.charAt(t[j]); + os.write(c); + } + } + return new String(os.toByteArray()); + } + + private static int lenToPadding(int blocklen) { + switch (blocklen) { + case 1: + return 6; + case 2: + return 4; + case 3: + return 3; + case 4: + return 1; + case 5: + return 0; + default: + return -1; + } + } + + private static int paddingToLen(int padlen) { + switch (padlen) { + case 6: + return 1; + case 4: + return 2; + case 3: + return 3; + case 1: + return 4; + case 0: + return 5; + default: + return -1; + } + } + +} diff --git a/source/org/jivesoftware/smack/util/Base64Encoder.java b/source/org/jivesoftware/smack/util/Base64Encoder.java new file mode 100644 index 000000000..78399b463 --- /dev/null +++ b/source/org/jivesoftware/smack/util/Base64Encoder.java @@ -0,0 +1,44 @@ +/** + * 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.smack.util; + + +/** + * @author Florian Schmaus + */ +public class Base64Encoder implements StringEncoder { + + private static Base64Encoder instance; + + private Base64Encoder() { + // Use getInstance() + } + + public static Base64Encoder getInstance() { + if (instance == null) { + instance = new Base64Encoder(); + } + return instance; + } + + public String encode(String s) { + return Base64.encodeBytes(s.getBytes()); + } + + public String decode(String s) { + return new String(Base64.decode(s)); + } + +} diff --git a/source/org/jivesoftware/smack/util/StringEncoder.java b/source/org/jivesoftware/smack/util/StringEncoder.java new file mode 100644 index 000000000..5a15c9548 --- /dev/null +++ b/source/org/jivesoftware/smack/util/StringEncoder.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. + */ + +/** + * @author Florian Schmaus + */ +package org.jivesoftware.smack.util; + +// TODO move StringEncoder, Base64Encoder and Base32Encoder to smack.util + +public interface StringEncoder { + /** + * Encodes an string to another representation + * + * @param string + * @return + */ + public String encode(String string); + + /** + * Decodes an string back to it's initial representation + * + * @param string + * @return + */ + public String decode(String string); +} diff --git a/source/org/jivesoftware/smackx/Form.java b/source/org/jivesoftware/smackx/Form.java index 8b654ab74..992c03619 100644 --- a/source/org/jivesoftware/smackx/Form.java +++ b/source/org/jivesoftware/smackx/Form.java @@ -42,17 +42,22 @@ import org.jivesoftware.smackx.packet.DataForm; * Depending of the form's type different operations are available. For example, it's only possible * to set answers if the form is of type "submit". * + * @see XEP-0004 Data Forms + * * @author Gaston Dombiak */ public class Form { - + public static final String TYPE_FORM = "form"; public static final String TYPE_SUBMIT = "submit"; public static final String TYPE_CANCEL = "cancel"; public static final String TYPE_RESULT = "result"; - + + public static final String NAMESPACE = "jabber:x:data"; + public static final String ELEMENT = "x"; + private DataForm dataForm; - + /** * Returns a new ReportedData if the packet is used for gathering data and includes an * extension that matches the elementName and namespace "x","jabber:x:data". diff --git a/source/org/jivesoftware/smackx/FormField.java b/source/org/jivesoftware/smackx/FormField.java index a10196032..44dfa8cec 100644 --- a/source/org/jivesoftware/smackx/FormField.java +++ b/source/org/jivesoftware/smackx/FormField.java @@ -299,6 +299,26 @@ public class FormField { return buf.toString(); } + public boolean equals(Object obj) { + if (obj == null) + return false; + if (obj == this) + return true; + if (obj.getClass() != getClass()) + return false; + + FormField other = (FormField) obj; + + String thisXml = toXML(); + String otherXml = other.toXML(); + + if (thisXml.equals(otherXml)) { + return true; + } else { + return false; + } + } + /** * Represents the available option of a given FormField. * @@ -354,5 +374,27 @@ public class FormField { buf.append(""); return buf.toString(); } + + public boolean equals(Object obj) { + if (obj == null) + return false; + if (obj == this) + return true; + if (obj.getClass() != getClass()) + return false; + + Option other = (Option) obj; + + if (!value.equals(other.value)) + return false; + + String thisLabel = label == null ? "" : label; + String otherLabel = other.label == null ? "" : other.label; + + if (!thisLabel.equals(otherLabel)) + return false; + + return true; + } } } diff --git a/source/org/jivesoftware/smackx/NodeInformationProvider.java b/source/org/jivesoftware/smackx/NodeInformationProvider.java index 816be4dba..68bb613d3 100644 --- a/source/org/jivesoftware/smackx/NodeInformationProvider.java +++ b/source/org/jivesoftware/smackx/NodeInformationProvider.java @@ -20,6 +20,7 @@ package org.jivesoftware.smackx; +import org.jivesoftware.smack.packet.PacketExtension; import org.jivesoftware.smackx.packet.DiscoverInfo; import org.jivesoftware.smackx.packet.DiscoverItems; @@ -36,7 +37,7 @@ import java.util.List; * @author Gaston Dombiak */ public interface NodeInformationProvider { - + /** * Returns a list of the Items {@link org.jivesoftware.smackx.packet.DiscoverItems.Item} * defined in the node. For example, the MUC protocol specifies that an XMPP client should @@ -65,4 +66,10 @@ public interface NodeInformationProvider { */ public abstract List getNodeIdentities(); + /** + * Returns a list of the packet extensions defined in the node. + * + * @return a list of the packet extensions defined in the node. + */ + public abstract List getNodePacketExtensions(); } diff --git a/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java b/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java index 8e184b765..f0e7912aa 100644 --- a/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java +++ b/source/org/jivesoftware/smackx/ServiceDiscoveryManager.java @@ -26,8 +26,11 @@ 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; @@ -47,8 +50,13 @@ import java.util.concurrent.ConcurrentHashMap; */ public class ServiceDiscoveryManager { - private static String identityName = "Smack"; - private static String identityType = "pc"; + 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 identities = new LinkedList(); + + private EntityCapsManager capsManager; private static Map instances = new ConcurrentHashMap(); @@ -66,6 +74,7 @@ public class ServiceDiscoveryManager { new ServiceDiscoveryManager(connection); } }); + identities.add(new Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE)); } /** @@ -77,6 +86,7 @@ public class ServiceDiscoveryManager { */ public ServiceDiscoveryManager(Connection connection) { this.connection = connection; + init(); } @@ -98,7 +108,12 @@ public class ServiceDiscoveryManager { * in a disco request. */ public static String getIdentityName() { - return identityName; + DiscoverInfo.Identity identity = identities.get(0); + if (identity != null) { + return identity.getName(); + } else { + return null; + } } /** @@ -109,7 +124,9 @@ public class ServiceDiscoveryManager { * in a disco request. */ public static void setIdentityName(String name) { - identityName = name; + DiscoverInfo.Identity identity = identities.remove(0); + identity = new DiscoverInfo.Identity(DEFAULT_IDENTITY_CATEGORY, name, DEFAULT_IDENTITY_TYPE); + identities.add(identity); } /** @@ -121,7 +138,12 @@ public class ServiceDiscoveryManager { * disco request. */ public static String getIdentityType() { - return identityType; + DiscoverInfo.Identity identity = identities.get(0); + if (identity != null) { + return identity.getType(); + } else { + return null; + } } /** @@ -133,7 +155,22 @@ public class ServiceDiscoveryManager { * disco request. */ public static void setIdentityType(String type) { - identityType = 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 getIdentities() { + return Collections.unmodifiableList(identities); } /** @@ -190,13 +227,10 @@ public class ServiceDiscoveryManager { NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverItems.getNode()); if (nodeInformationProvider != null) { - // Specified node was found - List items = nodeInformationProvider.getNodeItems(); - if (items != null) { - for (DiscoverItems.Item item : items) { - response.addItem(item); - } - } + // Specified node was found, add node items + response.addItems(nodeInformationProvider.getNodeItems()); + // Add packet extensions + response.addExtensions(nodeInformationProvider.getNodePacketExtensions()); } else if(discoverItems.getNode() != null) { // Return error since client doesn't contain // the specified node @@ -222,22 +256,12 @@ public class ServiceDiscoveryManager { response.setTo(discoverInfo.getFrom()); response.setPacketID(discoverInfo.getPacketID()); response.setNode(discoverInfo.getNode()); - // Add the client's identity and features only if "node" is null + // 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) { - // 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(it.next()); - } - if (extendedInfo != null) { - response.addExtension(extendedInfo); - } - } + addDiscoverInfoTo(response); } else { // Disco#info was sent to a node. Check if we have information of the @@ -246,20 +270,11 @@ public class ServiceDiscoveryManager { getNodeInformationProvider(discoverInfo.getNode()); if (nodeInformationProvider != null) { // Node was found. Add node features - List features = nodeInformationProvider.getNodeFeatures(); - if (features != null) { - for(String feature : features) { - response.addFeature(feature); - } - } + response.addFeatures(nodeInformationProvider.getNodeFeatures()); // Add node identities - List identities = - nodeInformationProvider.getNodeIdentities(); - if (identities != null) { - for (DiscoverInfo.Identity identity : identities) { - response.addIdentity(identity); - } - } + response.addIdentities(nodeInformationProvider.getNodeIdentities()); + // Add packet extensions + response.addExtensions(nodeInformationProvider.getNodePacketExtensions()); } else { // Return error since specified node was not found @@ -274,6 +289,26 @@ public class ServiceDiscoveryManager { connection.addPacketListener(packetListener, packetFilter); } + /** + * Add discover info response data. + * + * @see XEP-30 Basic Protocol; Example 2 + * + * @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 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 null if none.

@@ -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 { private String category; private String name; private String type; + private String lang; // 'xml:lang; /** * Creates a new identity for an XMPP entity. + * 'category' and 'type' are required by + * XEP-30 XML Schemas * - * @param category the entity's category. + * @param category the entity's category (required as per XEP-30). * @param name the entity's name. + * @param type the entity's type (required as per XEP-30). */ - public Identity(String category, String name) { + public Identity(String category, String name, String type) { this.category = category; this.name = name; + this.type = type; } /** @@ -223,16 +318,106 @@ public class DiscoverInfo extends IQ { this.type = type; } + /** + * Sets the natural language (xml:lang) for this identity (optional) + * + * @param lang the xml:lang of this Identity + */ + public void setLanguage(String lang) { + this.lang = lang; + } + + /** + * Returns the identities natural language if one is set + * + * @return the value of xml:lang of this Identity + */ + public String getLanguage() { + return lang; + } + public String toXML() { StringBuilder buf = new StringBuilder(); - buf.append(""); return buf.toString(); } + + /** + * Check equality for Identity for category, type, lang and name + * in that order as defined by + * XEP-0015 5.4 Processing Method (Step 3.3) + * + */ + public boolean equals(Object obj) { + if (obj == null) + return false; + if (obj == this) + return true; + if (obj.getClass() != getClass()) + return false; + + DiscoverInfo.Identity other = (DiscoverInfo.Identity) obj; + if (!this.category.equals(other.category)) + return false; + + String otherLang = other.lang == null ? "" : other.lang; + String thisLang = lang == null ? "" : lang; + + if (!other.type.equals(type)) + return false; + if (!otherLang.equals(thisLang)) + return false; + + String otherName = other.name == null ? "" : other.name; + String thisName = name == null ? "" : other.name; + if (!thisName.equals(otherName)) + return false; + + return true; + } + + /** + * Compares and identity with another object. The comparison order is: + * Category, Type, Lang. If all three are identical the other Identity is considered equal. + * Name is not used for comparision, as defined by XEP-0115 + * + * @param obj + * @return + */ + public int compareTo(Object obj) { + + DiscoverInfo.Identity other = (DiscoverInfo.Identity) obj; + String otherLang = other.lang == null ? "" : other.lang; + String thisLang = lang == null ? "" : lang; + + if (category.equals(other.category)) { + if (type.equals(other.type)) { + if (thisLang.equals(otherLang)) { + // Don't compare on name, XEP-30 says that name SHOULD + // be equals for all identities of an entity + return 0; + } else { + return thisLang.compareTo(otherLang); + } + } else { + return type.compareTo(other.type); + } + } else { + return category.compareTo(other.category); + } + } } /** @@ -268,5 +453,17 @@ public class DiscoverInfo extends IQ { buf.append(""); return buf.toString(); } + + public boolean equals(Object obj) { + if (obj == null) + return false; + if (obj == this) + return true; + if (obj.getClass() != getClass()) + return false; + + DiscoverInfo.Feature other = (DiscoverInfo.Feature) obj; + return variable.equals(other.variable); + } } } diff --git a/source/org/jivesoftware/smackx/packet/DiscoverItems.java b/source/org/jivesoftware/smackx/packet/DiscoverItems.java index f6b6dcae1..07185e68a 100644 --- a/source/org/jivesoftware/smackx/packet/DiscoverItems.java +++ b/source/org/jivesoftware/smackx/packet/DiscoverItems.java @@ -23,6 +23,7 @@ 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.List; @@ -55,6 +56,18 @@ public class DiscoverItems extends IQ { } } + /** + * Adds a collection of items to the discovered information. Does nothing if itemsToAdd is null + * + * @param itemsToAdd + */ + public void addItems(Collection itemsToAdd) { + if (itemsToAdd == null) return; + for (Item i : itemsToAdd) { + addItem(i); + } + } + /** * Returns the discovered items of the queried XMPP entity. * diff --git a/source/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java b/source/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java index cbed6208f..d10049160 100644 --- a/source/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java +++ b/source/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java @@ -42,6 +42,7 @@ public class DiscoverInfoProvider implements IQProvider { String name = ""; String type = ""; String variable = ""; + String lang = ""; discoverInfo.setNode(parser.getAttributeValue("", "node")); while (!done) { int eventType = parser.next(); @@ -51,6 +52,7 @@ public class DiscoverInfoProvider implements IQProvider { category = parser.getAttributeValue("", "category"); name = parser.getAttributeValue("", "name"); type = parser.getAttributeValue("", "type"); + lang = parser.getAttributeValue(parser.getNamespace("xml"), "lang"); } else if (parser.getName().equals("feature")) { // Initialize the variables from the parsed XML @@ -64,8 +66,9 @@ public class DiscoverInfoProvider implements IQProvider { } else if (eventType == XmlPullParser.END_TAG) { if (parser.getName().equals("identity")) { // Create a new identity and add it to the discovered info. - identity = new DiscoverInfo.Identity(category, name); - identity.setType(type); + identity = new DiscoverInfo.Identity(category, name, type); + if (lang != null) + identity.setLanguage(lang); discoverInfo.addIdentity(identity); } if (parser.getName().equals("feature")) { diff --git a/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java index 0a769c27c..5c7d9493c 100644 --- a/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java +++ b/test-unit/org/jivesoftware/smackx/bytestreams/socks5/Socks5ByteStreamManagerTest.java @@ -247,8 +247,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about NOT being a Socks5 // proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("noproxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("noproxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the proxy identity if proxy is queried @@ -312,8 +311,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about NOT being a Socks5 // proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("noproxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("noproxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the proxy identity if proxy is queried @@ -403,8 +401,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about being a SOCKS5 proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("proxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("proxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the socks5 bytestream proxy identity if proxy is queried @@ -494,8 +491,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about being a SOCKS5 proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("proxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("proxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the socks5 bytestream proxy identity if proxy is queried @@ -577,8 +573,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about being a SOCKS5 proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("proxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("proxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the socks5 bytestream proxy identity if proxy is queried @@ -672,8 +667,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about being a SOCKS5 proxy DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity = new Identity("proxy", proxyJID); - identity.setType("bytestreams"); + Identity identity = new Identity("proxy", proxyJID, "bytestreams"); proxyInfo.addIdentity(identity); // return the socks5 bytestream proxy identity if proxy is queried @@ -1026,7 +1020,7 @@ public class Socks5ByteStreamManagerTest { */ DiscoverInfo proxyInfo1 = Socks5PacketUtils.createDiscoverInfo("proxy2.xmpp-server", initiatorJID); - Identity identity1 = new Identity("proxy", "proxy2.xmpp-server"); + Identity identity1 = new Identity("proxy", "proxy2.xmpp-server", "bytestreams"); identity1.setType("bytestreams"); proxyInfo1.addIdentity(identity1); @@ -1036,8 +1030,7 @@ public class Socks5ByteStreamManagerTest { // build discover info for proxy containing information about being a SOCKS5 proxy DiscoverInfo proxyInfo2 = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); - Identity identity2 = new Identity("proxy", proxyJID); - identity2.setType("bytestreams"); + Identity identity2 = new Identity("proxy", proxyJID, "bytestreams"); proxyInfo2.addIdentity(identity2); // return the SOCKS5 bytestream proxy identity if proxy is queried diff --git a/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java b/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java new file mode 100644 index 000000000..754bf6d3c --- /dev/null +++ b/test-unit/org/jivesoftware/smackx/entitycaps/EntityCapsManagerTest.java @@ -0,0 +1,227 @@ +package org.jivesoftware.smackx.entitycaps; + +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedList; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.Base32Encoder; +import org.jivesoftware.smack.util.Base64Encoder; +import org.jivesoftware.smack.util.StringEncoder; +import org.jivesoftware.smackx.FormField; +import org.jivesoftware.smackx.entitycaps.EntityCapsManager; +import org.jivesoftware.smackx.entitycaps.cache.EntityCapsPersistentCache; +import org.jivesoftware.smackx.entitycaps.cache.SimpleDirectoryPersistentCache; +import org.jivesoftware.smackx.packet.DataForm; +import org.jivesoftware.smackx.packet.DiscoverInfo; +import org.junit.Test; + + +public class EntityCapsManagerTest { + + /** + * XEP- + * 0115 Complex Generation Example + */ + @Test + public void testComplexGenerationExample() { + DiscoverInfo di = createComplexSamplePacket(); + + String ver = EntityCapsManager.generateVerificationString(di, "sha-1"); + assertEquals("q07IKJEyjvHSyhy//CH0CxmKi8w=", ver); + } + + @Test + public void testSimpleDirectoryCacheBase64() throws IOException { + EntityCapsManager.persistentCache = null; + testSimpleDirectoryCache(Base64Encoder.getInstance()); + } + + @Test + public void testSimpleDirectoryCacheBase32() throws IOException { + EntityCapsManager.persistentCache = null; + testSimpleDirectoryCache(Base32Encoder.getInstance()); + } + + @Test + public void testVerificationDuplicateFeatures() { + DiscoverInfo di = createMalformedDiscoverInfo(); + assertTrue(di.containsDuplicateFeatures()); + } + + @Test + public void testVerificationDuplicateIdentities() { + DiscoverInfo di = createMalformedDiscoverInfo(); + assertTrue(di.containsDuplicateIdentities()); + } + + @Test + public void testVerificationDuplicateDataForm() { + DiscoverInfo di = createMalformedDiscoverInfo(); + assertTrue(EntityCapsManager.verifyPacketExtensions(di)); + } + + private void testSimpleDirectoryCache(StringEncoder stringEncoder) throws IOException { + + EntityCapsPersistentCache cache = new SimpleDirectoryPersistentCache(createTempDirectory()); + EntityCapsManager.setPersistentCache(cache); + + DiscoverInfo di = createComplexSamplePacket(); + String nodeVer = di.getNode() + "#" + EntityCapsManager.generateVerificationString(di, "sha-1"); + + // Save the data in EntityCapsManager + EntityCapsManager.addDiscoverInfoByNode(nodeVer, di); + + // Lose all the data + EntityCapsManager.caps.clear(); + + // Restore the data from the persistent Cache + cache.replay(); + + DiscoverInfo restored_di = EntityCapsManager.getDiscoveryInfoByNodeVer(nodeVer); + assertNotNull(restored_di); + assertEquals(di.toXML(), restored_di.toXML()); + } + + private static DiscoverInfo createComplexSamplePacket() { + DiscoverInfo di = new DiscoverInfo(); + di.setFrom("benvolio@capulet.lit/230193"); + di.setPacketID("disco1"); + di.setTo("juliet@capulet.lit/chamber"); + di.setType(IQ.Type.RESULT); + + Collection identities = new LinkedList(); + DiscoverInfo.Identity i = new DiscoverInfo.Identity("client", "Psi 0.11", "pc"); + i.setLanguage("en"); + identities.add(i); + i = new DiscoverInfo.Identity("client", "Ψ 0.11", "pc"); + i.setLanguage("el"); + identities.add(i); + di.addIdentities(identities); + + di.addFeature("http://jabber.org/protocol/disco#items"); + di.addFeature(EntityCapsManager.NAMESPACE); + di.addFeature("http://jabber.org/protocol/muc"); + di.addFeature("http://jabber.org/protocol/disco#info"); + + DataForm df = new DataForm("result"); + + FormField ff = new FormField("os"); + ff.addValue("Mac"); + df.addField(ff); + + ff = new FormField("FORM_TYPE"); + ff.setType("hidden"); + ff.addValue("urn:xmpp:dataforms:softwareinfo"); + df.addField(ff); + + ff = new FormField("ip_version"); + ff.addValue("ipv4"); + ff.addValue("ipv6"); + df.addField(ff); + + ff = new FormField("os_version"); + ff.addValue("10.5.1"); + df.addField(ff); + + ff = new FormField("software"); + ff.addValue("Psi"); + df.addField(ff); + + ff = new FormField("software_version"); + ff.addValue("0.11"); + df.addField(ff); + + di.addExtension(df); + return di; + } + + private static DiscoverInfo createMalformedDiscoverInfo() { + DiscoverInfo di = new DiscoverInfo(); + di.setFrom("benvolio@capulet.lit/230193"); + di.setPacketID("disco1"); + di.setTo(")juliet@capulet.lit/chamber"); + di.setType(IQ.Type.RESULT); + + Collection identities = new LinkedList(); + DiscoverInfo.Identity i = new DiscoverInfo.Identity("client", "Psi 0.11", "pc"); + i.setLanguage("en"); + identities.add(i); + i = new DiscoverInfo.Identity("client", "Ψ 0.11", "pc"); + i.setLanguage("el"); + identities.add(i); + di.addIdentities(identities); + // Failure 1: Duplicate identities + i = new DiscoverInfo.Identity("client", "Ψ 0.11", "pc"); + i.setLanguage("el"); + identities.add(i); + di.addIdentities(identities); + + di.addFeature("http://jabber.org/protocol/disco#items"); + di.addFeature(EntityCapsManager.NAMESPACE); + di.addFeature("http://jabber.org/protocol/muc"); + di.addFeature("http://jabber.org/protocol/disco#info"); + // Failure 2: Duplicate features + di.addFeature("http://jabber.org/protocol/disco#info"); + + DataForm df = new DataForm("result"); + + FormField ff = new FormField("os"); + ff.addValue("Mac"); + df.addField(ff); + + ff = new FormField("FORM_TYPE"); + ff.setType("hidden"); + ff.addValue("urn:xmpp:dataforms:softwareinfo"); + df.addField(ff); + + ff = new FormField("ip_version"); + ff.addValue("ipv4"); + ff.addValue("ipv6"); + df.addField(ff); + + ff = new FormField("os_version"); + ff.addValue("10.5.1"); + df.addField(ff); + + ff = new FormField("software"); + ff.addValue("Psi"); + df.addField(ff); + + ff = new FormField("software_version"); + ff.addValue("0.11"); + df.addField(ff); + + di.addExtension(df); + + // Failure 3: Another service discovery information form with the same + // FORM_TYPE + df = new DataForm("result"); + + ff = new FormField("FORM_TYPE"); + ff.setType("hidden"); + ff.addValue("urn:xmpp:dataforms:softwareinfo"); + df.addField(ff); + + ff = new FormField("software"); + ff.addValue("smack"); + df.addField(ff); + + di.addExtension(df); + + return di; + } + + public static File createTempDirectory() throws IOException { + String tmpdir = System.getProperty("java.io.tmpdir"); + File tmp; + tmp = File.createTempFile(tmpdir, "entityCaps"); + tmp.delete(); + tmp.mkdir(); + return tmp; + } + +} diff --git a/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java b/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java index 8f10e8e28..4b47aa2ff 100644 --- a/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java +++ b/test-unit/org/jivesoftware/smackx/pubsub/ConfigureFormTest.java @@ -50,8 +50,7 @@ public class ConfigureFormTest ThreadedDummyConnection con = new ThreadedDummyConnection(); PubSubManager mgr = new PubSubManager(con); DiscoverInfo info = new DiscoverInfo(); - Identity ident = new Identity("pubsub", null); - ident.setType("leaf"); + Identity ident = new Identity("pubsub", null, "leaf"); info.addIdentity(ident); con.addIQReply(info); @@ -78,8 +77,7 @@ public class ConfigureFormTest ThreadedDummyConnection con = new ThreadedDummyConnection(); PubSubManager mgr = new PubSubManager(con); DiscoverInfo info = new DiscoverInfo(); - Identity ident = new Identity("pubsub", null); - ident.setType("leaf"); + Identity ident = new Identity("pubsub", null, "leaf"); info.addIdentity(ident); con.addIQReply(info); diff --git a/test/org/jivesoftware/smack/ReconnectionTest.java b/test/org/jivesoftware/smack/ReconnectionTest.java index 8322ccef2..f4cbd0c32 100644 --- a/test/org/jivesoftware/smack/ReconnectionTest.java +++ b/test/org/jivesoftware/smack/ReconnectionTest.java @@ -40,7 +40,7 @@ public class ReconnectionTest extends SmackTestCase { public void testAutomaticReconnection() throws Exception { XMPPConnection connection = getConnection(0); - XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); + XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); connection.addConnectionListener(listener); // Simulates an error in the connection diff --git a/test/org/jivesoftware/smack/test/SmackTestCase.java b/test/org/jivesoftware/smack/test/SmackTestCase.java index 044a60cf4..fa6bb2d20 100644 --- a/test/org/jivesoftware/smack/test/SmackTestCase.java +++ b/test/org/jivesoftware/smack/test/SmackTestCase.java @@ -34,6 +34,7 @@ import junit.framework.TestCase; import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.util.ConnectionUtils; import org.xmlpull.mxp1.MXParser; import org.xmlpull.v1.XmlPullParser; @@ -504,6 +505,15 @@ public abstract class SmackTestCase extends TestCase { return "config/" + fullClassName.substring(firstChar) + ".xml"; } + /** + * Subscribes all connections with each other: They all become friends + * + * @throws XMPPException + */ + protected void letsAllBeFriends() throws XMPPException { + ConnectionUtils.letsAllBeFriends(connections); + } + /** * Compares two contents of two byte arrays to make sure that they are equal * diff --git a/test/org/jivesoftware/smack/util/ConnectionUtils.java b/test/org/jivesoftware/smack/util/ConnectionUtils.java new file mode 100644 index 000000000..6d9f49106 --- /dev/null +++ b/test/org/jivesoftware/smack/util/ConnectionUtils.java @@ -0,0 +1,27 @@ +package org.jivesoftware.smack.util; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.Roster; +import org.jivesoftware.smack.XMPPException; + +public class ConnectionUtils { + + public static void becomeFriends(Connection con0, Connection con1) throws XMPPException { + Roster r0 = con0.getRoster(); + Roster r1 = con1.getRoster(); + r0.setSubscriptionMode(Roster.SubscriptionMode.accept_all); + r1.setSubscriptionMode(Roster.SubscriptionMode.accept_all); + r0.createEntry(con1.getUser(), "u2", null); + r1.createEntry(con0.getUser(), "u1", null); + } + + public static void letsAllBeFriends(Connection[] connections) throws XMPPException { + for (Connection c1 : connections) { + for (Connection c2 : connections) { + if (c1 == c2) + continue; + becomeFriends(c1, c2); + } + } + } +} diff --git a/test/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java b/test/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java index 9a902ab31..9ba422e3e 100644 --- a/test/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java +++ b/test/org/jivesoftware/smackx/ServiceDiscoveryManagerTest.java @@ -48,7 +48,7 @@ public class ServiceDiscoveryManagerTest extends SmackTestCase { .getInstanceFor(getConnection(0)); try { // Discover the information of another Smack client - DiscoverInfo info = discoManager.discoverInfo(getFullJID(1)); + DiscoverInfo info = discoManager.discoverInfo(getFullJID(1)); // Check the identity of the Smack client Iterator identities = info.getIdentities(); assertTrue("No identities were found", identities.hasNext()); diff --git a/test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java b/test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java new file mode 100644 index 000000000..49a81f3e4 --- /dev/null +++ b/test/org/jivesoftware/smackx/entitycaps/EntityCapsTest.java @@ -0,0 +1,147 @@ +package org.jivesoftware.smackx.entitycaps; + +import org.jivesoftware.smack.PacketListener; +import org.jivesoftware.smack.SmackConfiguration; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.AndFilter; +import org.jivesoftware.smack.filter.PacketTypeFilter; +import org.jivesoftware.smack.filter.IQTypeFilter; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.test.SmackTestCase; +import org.jivesoftware.smackx.ServiceDiscoveryManager; +import org.jivesoftware.smackx.packet.DiscoverInfo; + +public class EntityCapsTest extends SmackTestCase { + + private static final String DISCOVER_TEST_FEATURE = "entityCapsTest"; + + XMPPConnection con0; + XMPPConnection con1; + EntityCapsManager ecm0; + EntityCapsManager ecm1; + ServiceDiscoveryManager sdm0; + ServiceDiscoveryManager sdm1; + + private boolean discoInfoSend = false; + + public EntityCapsTest(String arg0) { + super(arg0); + } + + @Override + protected int getMaxConnections() { + return 2; + } + + protected void setUp() throws Exception { + super.setUp(); + SmackConfiguration.setAutoEnableEntityCaps(true); + SmackConfiguration.setPacketReplyTimeout(1000 * 60 * 5); + con0 = getConnection(0); + con1 = getConnection(1); + ecm0 = EntityCapsManager.getInstanceFor(getConnection(0)); + ecm1 = EntityCapsManager.getInstanceFor(getConnection(1)); + sdm0 = ServiceDiscoveryManager.getInstanceFor(con0); + sdm1 = ServiceDiscoveryManager.getInstanceFor(con1); + letsAllBeFriends(); + } + + public void testLocalEntityCaps() throws InterruptedException { + DiscoverInfo info = EntityCapsManager.getDiscoveryInfoByNodeVer(ecm1.getLocalNodeVer()); + assertFalse(info.containsFeature(DISCOVER_TEST_FEATURE)); + + dropWholeEntityCapsCache(); + + // This should cause a new presence stanza from con1 with and updated + // 'ver' String + sdm1.addFeature(DISCOVER_TEST_FEATURE); + + // Give the server some time to handle the stanza and send it to con0 + Thread.sleep(2000); + + // The presence stanza should get received by con0 and the data should + // be recorded in the map + // Note that while both connections use the same static Entity Caps + // cache, + // it's assured that *not* con1 added the data to the Entity Caps cache. + // Every time the entities features + // and identities change only a new caps 'ver' is calculated and send + // with the presence stanza + // The other connection has to receive this stanza and record the + // information in order for this test to succeed. + info = EntityCapsManager.getDiscoveryInfoByNodeVer(ecm1.getLocalNodeVer()); + assertNotNull(info); + assertTrue(info.containsFeature(DISCOVER_TEST_FEATURE)); + } + + /** + * Test if entity caps actually prevent a disco info request and reply + * + * @throws XMPPException + * + */ + public void testPreventDiscoInfo() throws XMPPException { + con0.addPacketSendingListener(new PacketListener() { + + @Override + public void processPacket(Packet packet) { + discoInfoSend = true; + } + + }, new AndFilter(new PacketTypeFilter(DiscoverInfo.class), new IQTypeFilter(IQ.Type.GET))); + + // add a bogus feature so that con1 ver won't match con0's + sdm1.addFeature(DISCOVER_TEST_FEATURE); + + dropCapsCache(); + // discover that + DiscoverInfo info = sdm0.discoverInfo(con1.getUser()); + // that discovery should cause a disco#info + assertTrue(discoInfoSend); + assertTrue(info.containsFeature(DISCOVER_TEST_FEATURE)); + discoInfoSend = false; + + // discover that + info = sdm0.discoverInfo(con1.getUser()); + // that discovery shouldn't cause a disco#info + assertFalse(discoInfoSend); + assertTrue(info.containsFeature(DISCOVER_TEST_FEATURE)); + } + + public void testCapsChanged() { + String nodeVerBefore = EntityCapsManager.getNodeVersionByJid(con1.getUser()); + sdm1.addFeature(DISCOVER_TEST_FEATURE); + String nodeVerAfter = EntityCapsManager.getNodeVersionByJid(con1.getUser()); + + assertFalse(nodeVerBefore.equals(nodeVerAfter)); + } + + public void testEntityCaps() throws XMPPException, InterruptedException { + dropWholeEntityCapsCache(); + sdm1.addFeature(DISCOVER_TEST_FEATURE); + + Thread.sleep(3000); + + DiscoverInfo info = sdm0.discoverInfo(con1.getUser()); + assertTrue(info.containsFeature(DISCOVER_TEST_FEATURE)); + + String u1ver = EntityCapsManager.getNodeVersionByJid(con1.getUser()); + assertNotNull(u1ver); + + DiscoverInfo entityInfo = EntityCapsManager.caps.get(u1ver); + assertNotNull(entityInfo); + + assertEquals(info.toXML(), entityInfo.toXML()); + } + + private static void dropWholeEntityCapsCache() { + EntityCapsManager.caps.clear(); + EntityCapsManager.jidCaps.clear(); + } + + private static void dropCapsCache() { + EntityCapsManager.caps.clear(); + } +}