1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-11-27 00:32:07 +01:00

SMACK-361 Added support for Entity Capabilities.

git-svn-id: http://svn.igniterealtime.org/svn/repos/smack/branches/smack_3_3_0@13560 b35dd754-fafc-0310-a699-88a17e54d16e
This commit is contained in:
Florian Schmaus 2013-03-18 08:50:48 +00:00 committed by flow
parent 1cdb86989a
commit 21be8c55ee
33 changed files with 2395 additions and 88 deletions

View file

@ -34,4 +34,7 @@
<!-- Default interval (seconds) in which the Ping Manager sends ping request to the server (30 minutes) --> <!-- Default interval (seconds) in which the Ping Manager sends ping request to the server (30 minutes) -->
<defaultPingInterval>1800</defaultPingInterval> <defaultPingInterval>1800</defaultPingInterval>
<!-- Automatic enable Entity Caps (XEP-0115) for new connections -->
<autoEnableEntityCaps>false</autoEnableEntityCaps>
</smack> </smack>

View file

@ -678,4 +678,11 @@
<namespace>urn:xmpp:receipts</namespace> <namespace>urn:xmpp:receipts</namespace>
<className>org.jivesoftware.smackx.receipts.DeliveryReceiptRequest$Provider</className> <className>org.jivesoftware.smackx.receipts.DeliveryReceiptRequest$Provider</className>
</extensionProvider> </extensionProvider>
<!-- XEP-0115 Entity Capabilities -->
<extensionProvider>
<elementName>c</elementName>
<namespace>http://jabber.org/protocol/caps</namespace>
<className>org.jivesoftware.smackx.entitycaps.provider.CapsExtensionProvider</className>
</extensionProvider>
</smackProviders> </smackProviders>

View file

@ -204,6 +204,11 @@ public abstract class Connection {
*/ */
protected final ConnectionConfiguration config; 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; 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. * A wrapper class to associate a packet filter with a listener.

View file

@ -393,6 +393,19 @@ class PacketReader {
// The server requires the client to bind a resource to the stream // The server requires the client to bind a resource to the stream
connection.getSASLAuthentication().bindingRequired(); 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")) { else if (parser.getName().equals("session")) {
// The server supports sessions // The server supports sessions
connection.getSASLAuthentication().sessionsSupported(); connection.getSASLAuthentication().sessionsSupported();

View file

@ -63,6 +63,11 @@ public final class SmackConfiguration {
*/ */
private static int defaultPingInterval = 1800; // 30 min (30*60) 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() { private SmackConfiguration() {
} }
@ -115,6 +120,9 @@ public final class SmackConfiguration {
else if (parser.getName().equals("defaultPingInterval")) { else if (parser.getName().equals("defaultPingInterval")) {
defaultPingInterval = parseIntProperty(parser, defaultPingInterval); defaultPingInterval = parseIntProperty(parser, defaultPingInterval);
} }
else if (parser.getName().equals("autoEnableEntityCaps")) {
autoEnableEntityCaps = Boolean.parseBoolean(parser.nextText());
}
} }
eventType = parser.next(); eventType = parser.next();
} }
@ -329,6 +337,23 @@ public final class SmackConfiguration {
SmackConfiguration.defaultPingInterval = defaultPingInterval; 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 { private static void parseClassToLoad(XmlPullParser parser) throws Exception {
String className = parser.nextText(); String className = parser.nextText();
// Attempt to load the class so that the class can get initialized // Attempt to load the class so that the class can get initialized

View file

@ -43,6 +43,14 @@ public abstract class IQ extends Packet {
private Type type = Type.GET; private Type type = Type.GET;
public IQ() {
super();
}
public IQ(IQ iq) {
super(iq);
type = iq.getType();
}
/** /**
* Returns the type of the IQ packet. * Returns the type of the IQ packet.
* *

View file

@ -90,6 +90,22 @@ public abstract class Packet {
private final Map<String,Object> properties = new HashMap<String, Object>(); private final Map<String,Object> properties = new HashMap<String, Object>();
private XMPPError error = null; 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 <tt>null</tt> when * Returns the unique ID of the packet. The returned value could be <tt>null</tt> when
* ID_NOT_AVAILABLE was set as the packet's id. * 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. * @param extension a packet extension.
*/ */
public void addExtension(PacketExtension extension) { public void addExtension(PacketExtension extension) {
if (extension == null) return;
packetExtensions.add(extension); 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<PacketExtension> extensions) {
if (extensions == null) return;
packetExtensions.addAll(extensions);
}
/** /**
* Removes a packet extension from the packet. * Removes a packet extension from the packet.
* *
@ -266,7 +293,7 @@ public abstract class Packet {
/** /**
* Returns the packet property with the specified name or <tt>null</tt> if the * Returns the packet property with the specified name or <tt>null</tt> 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 * be returned as their object equivalent. For example, an int property will be
* returned as an Integer, a double as a Double, etc. * returned as an Integer, a double as a Double, etc.
* *

View file

@ -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 <a href="http://en.wikipedia.org/wiki/Base32">Base32 Wikipedia entry<a>
*
*/
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;
}
}
}

View file

@ -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));
}
}

View file

@ -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);
}

View file

@ -42,6 +42,8 @@ import org.jivesoftware.smackx.packet.DataForm;
* Depending of the form's type different operations are available. For example, it's only possible * 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". * to set answers if the form is of type "submit".
* *
* @see <a href="http://xmpp.org/extensions/xep-0004.html">XEP-0004 Data Forms</a>
*
* @author Gaston Dombiak * @author Gaston Dombiak
*/ */
public class Form { public class Form {
@ -51,6 +53,9 @@ public class Form {
public static final String TYPE_CANCEL = "cancel"; public static final String TYPE_CANCEL = "cancel";
public static final String TYPE_RESULT = "result"; public static final String TYPE_RESULT = "result";
public static final String NAMESPACE = "jabber:x:data";
public static final String ELEMENT = "x";
private DataForm dataForm; private DataForm dataForm;
/** /**

View file

@ -299,6 +299,26 @@ public class FormField {
return buf.toString(); 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. * Represents the available option of a given FormField.
* *
@ -354,5 +374,27 @@ public class FormField {
buf.append("</option>"); buf.append("</option>");
return buf.toString(); 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;
}
} }
} }

View file

@ -20,6 +20,7 @@
package org.jivesoftware.smackx; package org.jivesoftware.smackx;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smackx.packet.DiscoverInfo; import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.DiscoverItems; import org.jivesoftware.smackx.packet.DiscoverItems;
@ -65,4 +66,10 @@ public interface NodeInformationProvider {
*/ */
public abstract List<DiscoverInfo.Identity> getNodeIdentities(); public abstract List<DiscoverInfo.Identity> 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<PacketExtension> getNodePacketExtensions();
} }

View file

@ -26,8 +26,11 @@ import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter; import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
import org.jivesoftware.smackx.packet.DiscoverInfo; import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
import org.jivesoftware.smackx.packet.DiscoverItems; import org.jivesoftware.smackx.packet.DiscoverItems;
import org.jivesoftware.smackx.packet.DataForm; import org.jivesoftware.smackx.packet.DataForm;
@ -47,8 +50,13 @@ import java.util.concurrent.ConcurrentHashMap;
*/ */
public class ServiceDiscoveryManager { public class ServiceDiscoveryManager {
private static String identityName = "Smack"; private static final String DEFAULT_IDENTITY_NAME = "Smack";
private static String identityType = "pc"; private static final String DEFAULT_IDENTITY_CATEGORY = "client";
private static final String DEFAULT_IDENTITY_TYPE = "pc";
private static List<DiscoverInfo.Identity> identities = new LinkedList<DiscoverInfo.Identity>();
private EntityCapsManager capsManager;
private static Map<Connection, ServiceDiscoveryManager> instances = private static Map<Connection, ServiceDiscoveryManager> instances =
new ConcurrentHashMap<Connection, ServiceDiscoveryManager>(); new ConcurrentHashMap<Connection, ServiceDiscoveryManager>();
@ -66,6 +74,7 @@ public class ServiceDiscoveryManager {
new ServiceDiscoveryManager(connection); 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) { public ServiceDiscoveryManager(Connection connection) {
this.connection = connection; this.connection = connection;
init(); init();
} }
@ -98,7 +108,12 @@ public class ServiceDiscoveryManager {
* in a disco request. * in a disco request.
*/ */
public static String getIdentityName() { 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. * in a disco request.
*/ */
public static void setIdentityName(String name) { 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. * disco request.
*/ */
public static String getIdentityType() { 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. * disco request.
*/ */
public static void setIdentityType(String type) { 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<DiscoverInfo.Identity> getIdentities() {
return Collections.unmodifiableList(identities);
} }
/** /**
@ -190,13 +227,10 @@ public class ServiceDiscoveryManager {
NodeInformationProvider nodeInformationProvider = NodeInformationProvider nodeInformationProvider =
getNodeInformationProvider(discoverItems.getNode()); getNodeInformationProvider(discoverItems.getNode());
if (nodeInformationProvider != null) { if (nodeInformationProvider != null) {
// Specified node was found // Specified node was found, add node items
List<DiscoverItems.Item> items = nodeInformationProvider.getNodeItems(); response.addItems(nodeInformationProvider.getNodeItems());
if (items != null) { // Add packet extensions
for (DiscoverItems.Item item : items) { response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
response.addItem(item);
}
}
} else if(discoverItems.getNode() != null) { } else if(discoverItems.getNode() != null) {
// Return <item-not-found/> error since client doesn't contain // Return <item-not-found/> error since client doesn't contain
// the specified node // the specified node
@ -222,22 +256,12 @@ public class ServiceDiscoveryManager {
response.setTo(discoverInfo.getFrom()); response.setTo(discoverInfo.getFrom());
response.setPacketID(discoverInfo.getPacketID()); response.setPacketID(discoverInfo.getPacketID());
response.setNode(discoverInfo.getNode()); 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) { if (discoverInfo.getNode() == null) {
// Set this client identity addDiscoverInfoTo(response);
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<String> it = getFeatures(); it.hasNext();) {
response.addFeature(it.next());
}
if (extendedInfo != null) {
response.addExtension(extendedInfo);
}
}
} }
else { else {
// Disco#info was sent to a node. Check if we have information of the // Disco#info was sent to a node. Check if we have information of the
@ -246,20 +270,11 @@ public class ServiceDiscoveryManager {
getNodeInformationProvider(discoverInfo.getNode()); getNodeInformationProvider(discoverInfo.getNode());
if (nodeInformationProvider != null) { if (nodeInformationProvider != null) {
// Node was found. Add node features // Node was found. Add node features
List<String> features = nodeInformationProvider.getNodeFeatures(); response.addFeatures(nodeInformationProvider.getNodeFeatures());
if (features != null) {
for(String feature : features) {
response.addFeature(feature);
}
}
// Add node identities // Add node identities
List<DiscoverInfo.Identity> identities = response.addIdentities(nodeInformationProvider.getNodeIdentities());
nodeInformationProvider.getNodeIdentities(); // Add packet extensions
if (identities != null) { response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
for (DiscoverInfo.Identity identity : identities) {
response.addIdentity(identity);
}
}
} }
else { else {
// Return <item-not-found/> error since specified node was not found // Return <item-not-found/> error since specified node was not found
@ -274,6 +289,26 @@ public class ServiceDiscoveryManager {
connection.addPacketListener(packetListener, packetFilter); connection.addPacketListener(packetListener, packetFilter);
} }
/**
* Add discover info response data.
*
* @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol; Example 2</a>
*
* @param response the discover info response packet
*/
public void addDiscoverInfoTo(DiscoverInfo response) {
// First add the identities of the connection
response.addIdentities(identities);
// Add the registered features to the response
synchronized (features) {
for (Iterator<String> it = getFeatures(); it.hasNext();) {
response.addFeature(it.next());
}
response.addExtension(extendedInfo);
}
}
/** /**
* Returns the NodeInformationProvider responsible for providing information * Returns the NodeInformationProvider responsible for providing information
* (ie items) related to a given node or <tt>null</null> if none.<p> * (ie items) related to a given node or <tt>null</null> if none.<p>
@ -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<String> getFeaturesList() {
synchronized (features) {
return new LinkedList<String>(features);
}
}
/** /**
* Registers that a new feature is supported by this XMPP entity. When this client is * Registers that a new feature is supported by this XMPP entity. When this client is
* queried for its information the registered features will be answered.<p> * queried for its information the registered features will be answered.<p>
@ -348,6 +394,7 @@ public class ServiceDiscoveryManager {
public void addFeature(String feature) { public void addFeature(String feature) {
synchronized (features) { synchronized (features) {
features.add(feature); features.add(feature);
renewEntityCapsVersion();
} }
} }
@ -362,6 +409,7 @@ public class ServiceDiscoveryManager {
public void removeFeature(String feature) { public void removeFeature(String feature) {
synchronized (features) { synchronized (features) {
features.remove(feature); features.remove(feature);
renewEntityCapsVersion();
} }
} }
@ -394,10 +442,36 @@ public class ServiceDiscoveryManager {
*/ */
public void setExtendedInfo(DataForm info) { public void setExtendedInfo(DataForm info) {
extendedInfo = 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 <a href="http://xmpp.org/extensions/xep-0128.html">XEP-128: Service Discovery Extensions</a>
* @return
*/
public DataForm getExtendedInfo() {
return extendedInfo;
}
/**
* Returns the data form as List of PacketExtensions, or null if no data form is set.
* This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider)
*
* @return
*/
public List<PacketExtension> getExtendedInfoAsList() {
List<PacketExtension> res = null;
if (extendedInfo != null) {
res = new ArrayList<PacketExtension>(1);
res.add(extendedInfo);
}
return res;
}
/**
* Removes the data form containing extended service discovery information
* from the information returned by this XMPP entity.<p> * from the information returned by this XMPP entity.<p>
* *
* Since no packet is actually sent to the server it is safe to perform this * 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() { public void removeExtendedInfo() {
extendedInfo = null; extendedInfo = null;
renewEntityCapsVersion();
} }
/** /**
* Returns the discovered information of a given XMPP entity addressed by its JID. * 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. * @return the discovered information.
* @throws XMPPException if the operation failed for some reason. * @throws XMPPException if the operation failed for some reason.
*/ */
public DiscoverInfo discoverInfo(String entityID) throws XMPPException { 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 * note attribute. Use this message only when trying to query information which is not
* directly addressable. * directly addressable.
* *
* @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a>
* @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a>
*
* @param entityID the address of the XMPP entity. * @param 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. * @return the discovered information.
* @throws XMPPException if the operation failed for some reason. * @throws XMPPException if the operation failed for some reason.
*/ */
@ -471,7 +576,7 @@ public class ServiceDiscoveryManager {
* directly addressable. * directly addressable.
* *
* @param entityID the address of the XMPP entity. * @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. * @return the discovered items.
* @throws XMPPException if the operation failed for some reason. * @throws XMPPException if the operation failed for some reason.
*/ */
@ -513,8 +618,21 @@ public class ServiceDiscoveryManager {
*/ */
public boolean canPublishItems(String entityID) throws XMPPException { public boolean canPublishItems(String entityID) throws XMPPException {
DiscoverInfo info = discoverInfo(entityID); 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 * 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()); throw new XMPPException(result.getError());
} }
} }
/**
* Entity Capabilities
*/
/**
* Loads the ServiceDiscoveryManager with an EntityCapsManger
* that speeds up certain lookups
* @param manager
*/
public void setEntityCapsManager(EntityCapsManager manager) {
capsManager = manager;
}
/**
* Updates the Entity Capabilities Verification String
* if EntityCaps is enabled
*/
private void renewEntityCapsVersion() {
if (capsManager != null && capsManager.entityCapsEnabled())
capsManager.updateLocalEntityCaps();
}
} }

View file

@ -25,6 +25,7 @@ import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter; import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.packet.XMPPError;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.Form; import org.jivesoftware.smackx.Form;
@ -181,12 +182,16 @@ public class AdHocCommandManager {
public List<DiscoverInfo.Identity> getNodeIdentities() { public List<DiscoverInfo.Identity> getNodeIdentities() {
List<DiscoverInfo.Identity> answer = new ArrayList<DiscoverInfo.Identity>(); List<DiscoverInfo.Identity> answer = new ArrayList<DiscoverInfo.Identity>();
DiscoverInfo.Identity identity = new DiscoverInfo.Identity( DiscoverInfo.Identity identity = new DiscoverInfo.Identity(
"automation", name); "automation", name, "command-node");
identity.setType("command-node");
answer.add(identity); answer.add(identity);
return answer; return answer;
} }
@Override
public List<PacketExtension> getNodePacketExtensions() {
return null;
}
}); });
} }
@ -319,6 +324,11 @@ public class AdHocCommandManager {
public List<Identity> getNodeIdentities() { public List<Identity> getNodeIdentities() {
return null; return null;
} }
@Override
public List<PacketExtension> getNodePacketExtensions() {
return null;
}
}); });
// The packet listener and the filter for processing some AdHoc Commands // The packet listener and the filter for processing some AdHoc Commands

View file

@ -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<String, MessageDigest> SUPPORTED_HASHES = new HashMap<String, MessageDigest>();
protected static EntityCapsPersistentCache persistentCache;
private static Map<Connection, EntityCapsManager> instances = Collections
.synchronizedMap(new WeakHashMap<Connection, EntityCapsManager>());
/**
* Map of (node + '#" + hash algorithm) to DiscoverInfo data
*/
protected static Map<String, DiscoverInfo> caps = new Cache<String, DiscoverInfo>(1000, -1);
/**
* Map of Full JID -&gt; 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<String, NodeVerHash> jidCaps = new Cache<String, NodeVerHash>(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<Connection> weakRefConnection;
private ServiceDiscoveryManager sdm;
private boolean entityCapsEnabled;
private String currentCapsVersion;
private boolean presenceSend = false;
private Queue<String> lastLocalCapsVersions = new ConcurrentLinkedQueue<String>();
/**
* 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>(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<String> features = sdm.getFeaturesList();
List<Identity> identities = new LinkedList<Identity>(ServiceDiscoveryManager.getIdentities());
List<PacketExtension> packetExtensions = sdm.getExtendedInfoAsList();
@Override
public List<Item> getNodeItems() {
return null;
}
@Override
public List<String> getNodeFeatures() {
return features;
}
@Override
public List<Identity> getNodeIdentities() {
return identities;
}
@Override
public List<PacketExtension> getNodePacketExtensions() {
return packetExtensions;
}
});
// Send an empty presence, and let the packet intercepter
// add a <c/> 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 <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0115
* 5.4 Processing Method</a>
*
* @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<FormField> foundFormTypes = new LinkedList<FormField>();
for (Iterator<PacketExtension> i = info.getExtensions().iterator(); i.hasNext();) {
PacketExtension pe = i.next();
if (pe.getNamespace().equals(Form.NAMESPACE)) {
DataForm df = (DataForm) pe;
for (Iterator<FormField> 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 <a href="http://xmpp.org/extensions/xep-0115.html#ver">XEP-115
* Verification String</a>
*
* @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<DiscoverInfo.Identity> sortedIdentities = new TreeSet<DiscoverInfo.Identity>();
;
for (Iterator<DiscoverInfo.Identity> 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<DiscoverInfo.Identity> 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<String> features = new TreeSet<String>();
for (Iterator<Feature> 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 <value/> element).
SortedSet<FormField> fs = new TreeSet<FormField>(new Comparator<FormField>() {
public int compare(FormField f1, FormField f2) {
return f1.getVariable().compareTo(f2.getVariable());
}
});
FormField ft = null;
for (Iterator<FormField> 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 <value/>
// element.
// 3. For each <value/> 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<String> i, StringBuilder sb) {
SortedSet<String> fvs = new TreeSet<String>();
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;
}
}
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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;
}
/*
* <c xmlns='http://jabber.org/protocol/caps'
* hash='sha-1'
* node='http://code.google.com/p/exodus'
* ver='QgayPKawpkPSDYmwT/WM94uAlu0='/>
*
*/
public String toXML() {
String xml = "<" + EntityCapsManager.ELEMENT + " xmlns=\"" + EntityCapsManager.NAMESPACE + "\" " +
"hash=\"" + hash + "\" " +
"node=\"" + node + "\" " +
"ver=\"" + ver + "\"/>";
return xml;
}
}

View file

@ -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");
}
}
}

View file

@ -52,6 +52,7 @@ import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Registration; import org.jivesoftware.smack.packet.Registration;
import org.jivesoftware.smackx.Form; import org.jivesoftware.smackx.Form;
@ -133,6 +134,11 @@ public class MultiUserChat {
public List<DiscoverInfo.Identity> getNodeIdentities() { public List<DiscoverInfo.Identity> getNodeIdentities() {
return null; return null;
} }
@Override
public List<PacketExtension> getNodePacketExtensions() {
return null;
}
}); });
} }
}); });

View file

@ -21,6 +21,7 @@
package org.jivesoftware.smackx.packet; package org.jivesoftware.smackx.packet;
import org.jivesoftware.smack.packet.PacketExtension; import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.FormField; import org.jivesoftware.smackx.FormField;
import java.util.ArrayList; import java.util.ArrayList;
@ -123,11 +124,11 @@ public class DataForm implements PacketExtension {
} }
public String getElementName() { public String getElementName() {
return "x"; return Form.ELEMENT;
} }
public String getNamespace() { 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() { public String toXML() {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append( buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(

View file

@ -23,8 +23,10 @@ package org.jivesoftware.smackx.packet;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@ -45,6 +47,36 @@ public class DiscoverInfo extends IQ {
private final List<Identity> identities = new CopyOnWriteArrayList<Identity>(); private final List<Identity> identities = new CopyOnWriteArrayList<Identity>();
private String node; 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. * Adds a new feature to the discovered information.
* *
@ -54,6 +86,18 @@ public class DiscoverInfo extends IQ {
addFeature(new Feature(feature)); addFeature(new Feature(feature));
} }
/**
* Adds a collection of features to the packet. Does noting if featuresToAdd is null.
*
* @param featuresToAdd
*/
public void addFeatures(Collection<String> featuresToAdd) {
if (featuresToAdd == null) return;
for (String feature : featuresToAdd) {
addFeature(feature);
}
}
private void addFeature(Feature feature) { private void addFeature(Feature feature) {
synchronized (features) { synchronized (features) {
features.add(feature); features.add(feature);
@ -82,6 +126,18 @@ public class DiscoverInfo extends IQ {
} }
} }
/**
* Adds identities to the DiscoverInfo stanza
*
* @param identitiesToAdd
*/
public void addIdentities(Collection<Identity> identitiesToAdd) {
if (identitiesToAdd == null) return;
synchronized (identities) {
identities.addAll(identitiesToAdd);
}
}
/** /**
* Returns the discovered identities of an XMPP entity. * Returns the discovered identities of an XMPP entity.
* *
@ -158,6 +214,40 @@ public class DiscoverInfo extends IQ {
return buf.toString(); return buf.toString();
} }
/**
* Test if a DiscoverInfo response contains duplicate identities.
*
* @return true if duplicate identities where found, otherwise false
*/
public boolean containsDuplicateIdentities() {
List<Identity> checkedIdentities = new LinkedList<Identity>();
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<Feature> checkedFeatures = new LinkedList<Feature>();
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 * Represents the identity of a given XMPP entity. An entity may have many identities but all
* the identities SHOULD have the same name.<p> * the identities SHOULD have the same name.<p>
@ -167,21 +257,26 @@ public class DiscoverInfo extends IQ {
* attributes. * attributes.
* *
*/ */
public static class Identity { public static class Identity implements Comparable<Object> {
private String category; private String category;
private String name; private String name;
private String type; private String type;
private String lang; // 'xml:lang;
/** /**
* Creates a new identity for an XMPP entity. * Creates a new identity for an XMPP entity.
* 'category' and 'type' are required by
* <a href="http://xmpp.org/extensions/xep-0030.html#schemas">XEP-30 XML Schemas</a>
* *
* @param category the entity's category. * @param category the entity's category (required as per XEP-30).
* @param name the entity's name. * @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.category = category;
this.name = name; this.name = name;
this.type = type;
} }
/** /**
@ -223,16 +318,106 @@ public class DiscoverInfo extends IQ {
this.type = type; 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() { public String toXML() {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
buf.append("<identity category=\"").append(StringUtils.escapeForXML(category)).append("\""); buf.append("<identity");
// Check if this packet has 'lang' set and maybe append it to the resulting string
if (lang != null)
buf.append(" xml:lang=\"").append(StringUtils.escapeForXML(lang)).append("\"");
// Category must always be set
buf.append(" category=\"").append(StringUtils.escapeForXML(category)).append("\"");
// Name must always be set
buf.append(" name=\"").append(StringUtils.escapeForXML(name)).append("\""); buf.append(" name=\"").append(StringUtils.escapeForXML(name)).append("\"");
// Check if this packet has 'type' set and maybe append it to the resulting string
if (type != null) { if (type != null) {
buf.append(" type=\"").append(StringUtils.escapeForXML(type)).append("\""); buf.append(" type=\"").append(StringUtils.escapeForXML(type)).append("\"");
} }
buf.append("/>"); buf.append("/>");
return buf.toString(); return buf.toString();
} }
/**
* Check equality for Identity for category, type, lang and name
* in that order as defined by
* <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0015 5.4 Processing Method (Step 3.3)</a>
*
*/
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("<feature var=\"").append(StringUtils.escapeForXML(variable)).append("\"/>"); buf.append("<feature var=\"").append(StringUtils.escapeForXML(variable)).append("\"/>");
return buf.toString(); 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);
}
} }
} }

View file

@ -23,6 +23,7 @@ package org.jivesoftware.smackx.packet;
import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.StringUtils;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; 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<Item> itemsToAdd) {
if (itemsToAdd == null) return;
for (Item i : itemsToAdd) {
addItem(i);
}
}
/** /**
* Returns the discovered items of the queried XMPP entity. * Returns the discovered items of the queried XMPP entity.
* *

View file

@ -42,6 +42,7 @@ public class DiscoverInfoProvider implements IQProvider {
String name = ""; String name = "";
String type = ""; String type = "";
String variable = ""; String variable = "";
String lang = "";
discoverInfo.setNode(parser.getAttributeValue("", "node")); discoverInfo.setNode(parser.getAttributeValue("", "node"));
while (!done) { while (!done) {
int eventType = parser.next(); int eventType = parser.next();
@ -51,6 +52,7 @@ public class DiscoverInfoProvider implements IQProvider {
category = parser.getAttributeValue("", "category"); category = parser.getAttributeValue("", "category");
name = parser.getAttributeValue("", "name"); name = parser.getAttributeValue("", "name");
type = parser.getAttributeValue("", "type"); type = parser.getAttributeValue("", "type");
lang = parser.getAttributeValue(parser.getNamespace("xml"), "lang");
} }
else if (parser.getName().equals("feature")) { else if (parser.getName().equals("feature")) {
// Initialize the variables from the parsed XML // Initialize the variables from the parsed XML
@ -64,8 +66,9 @@ public class DiscoverInfoProvider implements IQProvider {
} else if (eventType == XmlPullParser.END_TAG) { } else if (eventType == XmlPullParser.END_TAG) {
if (parser.getName().equals("identity")) { if (parser.getName().equals("identity")) {
// Create a new identity and add it to the discovered info. // Create a new identity and add it to the discovered info.
identity = new DiscoverInfo.Identity(category, name); identity = new DiscoverInfo.Identity(category, name, type);
identity.setType(type); if (lang != null)
identity.setLanguage(lang);
discoverInfo.addIdentity(identity); discoverInfo.addIdentity(identity);
} }
if (parser.getName().equals("feature")) { if (parser.getName().equals("feature")) {

View file

@ -247,8 +247,7 @@ public class Socks5ByteStreamManagerTest {
// build discover info for proxy containing information about NOT being a Socks5 // build discover info for proxy containing information about NOT being a Socks5
// proxy // proxy
DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("noproxy", proxyJID); Identity identity = new Identity("noproxy", proxyJID, "bytestreams");
identity.setType("bytestreams");
proxyInfo.addIdentity(identity); proxyInfo.addIdentity(identity);
// return the proxy identity if proxy is queried // 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 // build discover info for proxy containing information about NOT being a Socks5
// proxy // proxy
DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("noproxy", proxyJID); Identity identity = new Identity("noproxy", proxyJID, "bytestreams");
identity.setType("bytestreams");
proxyInfo.addIdentity(identity); proxyInfo.addIdentity(identity);
// return the proxy identity if proxy is queried // 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 // build discover info for proxy containing information about being a SOCKS5 proxy
DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("proxy", proxyJID); Identity identity = new Identity("proxy", proxyJID, "bytestreams");
identity.setType("bytestreams");
proxyInfo.addIdentity(identity); proxyInfo.addIdentity(identity);
// return the socks5 bytestream proxy identity if proxy is queried // 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 // build discover info for proxy containing information about being a SOCKS5 proxy
DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("proxy", proxyJID); Identity identity = new Identity("proxy", proxyJID, "bytestreams");
identity.setType("bytestreams");
proxyInfo.addIdentity(identity); proxyInfo.addIdentity(identity);
// return the socks5 bytestream proxy identity if proxy is queried // 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 // build discover info for proxy containing information about being a SOCKS5 proxy
DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("proxy", proxyJID); Identity identity = new Identity("proxy", proxyJID, "bytestreams");
identity.setType("bytestreams");
proxyInfo.addIdentity(identity); proxyInfo.addIdentity(identity);
// return the socks5 bytestream proxy identity if proxy is queried // 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 // build discover info for proxy containing information about being a SOCKS5 proxy
DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); DiscoverInfo proxyInfo = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity = new Identity("proxy", proxyJID); Identity identity = new Identity("proxy", proxyJID, "bytestreams");
identity.setType("bytestreams");
proxyInfo.addIdentity(identity); proxyInfo.addIdentity(identity);
// return the socks5 bytestream proxy identity if proxy is queried // return the socks5 bytestream proxy identity if proxy is queried
@ -1026,7 +1020,7 @@ public class Socks5ByteStreamManagerTest {
*/ */
DiscoverInfo proxyInfo1 = Socks5PacketUtils.createDiscoverInfo("proxy2.xmpp-server", DiscoverInfo proxyInfo1 = Socks5PacketUtils.createDiscoverInfo("proxy2.xmpp-server",
initiatorJID); initiatorJID);
Identity identity1 = new Identity("proxy", "proxy2.xmpp-server"); Identity identity1 = new Identity("proxy", "proxy2.xmpp-server", "bytestreams");
identity1.setType("bytestreams"); identity1.setType("bytestreams");
proxyInfo1.addIdentity(identity1); proxyInfo1.addIdentity(identity1);
@ -1036,8 +1030,7 @@ public class Socks5ByteStreamManagerTest {
// build discover info for proxy containing information about being a SOCKS5 proxy // build discover info for proxy containing information about being a SOCKS5 proxy
DiscoverInfo proxyInfo2 = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID); DiscoverInfo proxyInfo2 = Socks5PacketUtils.createDiscoverInfo(proxyJID, initiatorJID);
Identity identity2 = new Identity("proxy", proxyJID); Identity identity2 = new Identity("proxy", proxyJID, "bytestreams");
identity2.setType("bytestreams");
proxyInfo2.addIdentity(identity2); proxyInfo2.addIdentity(identity2);
// return the SOCKS5 bytestream proxy identity if proxy is queried // return the SOCKS5 bytestream proxy identity if proxy is queried

View file

@ -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 {
/**
* <a href="http://xmpp.org/extensions/xep-0115.html#ver-gen-complex">XEP-
* 0115 Complex Generation Example</a>
*/
@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<DiscoverInfo.Identity> identities = new LinkedList<DiscoverInfo.Identity>();
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<DiscoverInfo.Identity> identities = new LinkedList<DiscoverInfo.Identity>();
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;
}
}

View file

@ -50,8 +50,7 @@ public class ConfigureFormTest
ThreadedDummyConnection con = new ThreadedDummyConnection(); ThreadedDummyConnection con = new ThreadedDummyConnection();
PubSubManager mgr = new PubSubManager(con); PubSubManager mgr = new PubSubManager(con);
DiscoverInfo info = new DiscoverInfo(); DiscoverInfo info = new DiscoverInfo();
Identity ident = new Identity("pubsub", null); Identity ident = new Identity("pubsub", null, "leaf");
ident.setType("leaf");
info.addIdentity(ident); info.addIdentity(ident);
con.addIQReply(info); con.addIQReply(info);
@ -78,8 +77,7 @@ public class ConfigureFormTest
ThreadedDummyConnection con = new ThreadedDummyConnection(); ThreadedDummyConnection con = new ThreadedDummyConnection();
PubSubManager mgr = new PubSubManager(con); PubSubManager mgr = new PubSubManager(con);
DiscoverInfo info = new DiscoverInfo(); DiscoverInfo info = new DiscoverInfo();
Identity ident = new Identity("pubsub", null); Identity ident = new Identity("pubsub", null, "leaf");
ident.setType("leaf");
info.addIdentity(ident); info.addIdentity(ident);
con.addIQReply(info); con.addIQReply(info);

View file

@ -34,6 +34,7 @@ import junit.framework.TestCase;
import org.jivesoftware.smack.ConnectionConfiguration; import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.XMPPConnection; import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.util.ConnectionUtils;
import org.xmlpull.mxp1.MXParser; import org.xmlpull.mxp1.MXParser;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
@ -504,6 +505,15 @@ public abstract class SmackTestCase extends TestCase {
return "config/" + fullClassName.substring(firstChar) + ".xml"; 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 * Compares two contents of two byte arrays to make sure that they are equal
* *

View file

@ -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);
}
}
}
}

View file

@ -48,7 +48,7 @@ public class ServiceDiscoveryManagerTest extends SmackTestCase {
.getInstanceFor(getConnection(0)); .getInstanceFor(getConnection(0));
try { try {
// Discover the information of another Smack client // 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 // Check the identity of the Smack client
Iterator<Identity> identities = info.getIdentities(); Iterator<Identity> identities = info.getIdentities();
assertTrue("No identities were found", identities.hasNext()); assertTrue("No identities were found", identities.hasNext());

View file

@ -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();
}
}