diff --git a/resources/smack.providers b/resources/smack.providers
index e06e34e2d..a375f3da8 100644
--- a/resources/smack.providers
+++ b/resources/smack.providers
@@ -6,4 +6,9 @@
jabber:iq:private
org.jivesoftware.smackx.packet.PrivateDataManager.PrivateDataIQProvider
+
+ x
+ jabber:x:roster
+ org.jivesoftware.smackx.provider.RosterExchangeProvider
+
\ No newline at end of file
diff --git a/source/org/jivesoftware/smackx/packet/RosterExchange.java b/source/org/jivesoftware/smackx/packet/RosterExchange.java
new file mode 100644
index 000000000..e9b3f7376
--- /dev/null
+++ b/source/org/jivesoftware/smackx/packet/RosterExchange.java
@@ -0,0 +1,261 @@
+/*
+ * Created on 27/07/2003
+ */
+package org.jivesoftware.smackx.packet;
+
+import java.util.*;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents XMPP Roster Item Exchange packets.
+ * The 'jabber:x:roster' namespace (which is not to be confused with the 'jabber:iq:roster' namespace) is used to
+ * send roster items from one client to another. A roster item is sent by adding to the element an child
+ * scoped by the 'jabber:x:roster' namespace. This element may contain one or more children (one for each roster item to be sent).
+ *
+ * Each element may possess the following attributes:
+ *
+ * -- The id of the contact being sent. This attribute is required.
+ * -- A natural-language nickname for the contact. This attribute is optional.
+ *
+ * Each element may also contain one or more children specifying the natural-language name
+ * of a user-specified group, for the purpose of categorizing this contact into one or more roster groups.
+ *
+ * @author Gaston Dombiak
+ */
+public class RosterExchange implements PacketExtension {
+
+ private List rosterItems = new ArrayList();
+
+ /**
+ * Creates a new empty roster exchange package.
+ *
+ */
+ public RosterExchange(){
+ super();
+ }
+
+ /**
+ * Creates a new roster exchange package with the entries specified in roster.
+ *
+ * @param roster the roster to send to other XMPP entity.
+ */
+ public RosterExchange(Roster roster){
+ RosterGroup rosterGroup = null;
+ // Loop through all roster groups and add their entries to the new RosterExchange
+ for (Iterator groups = roster.getGroups(); groups.hasNext(); ) {
+ rosterGroup = (RosterGroup) groups.next();
+ for (Iterator entries = rosterGroup.getEntries(); entries.hasNext(); ) {
+ this.addRosterItem((RosterEntry) entries.next());
+ }
+ }
+ // Add the roster unfiled entries to the new RosterExchange
+ for (Iterator unfiledEntries = roster.getUnfiledEntries(); unfiledEntries.hasNext();) {
+ this.addRosterItem((RosterEntry) unfiledEntries.next());
+ }
+ }
+
+ /**
+ * Adds a roster entry to the packet.
+ *
+ * @param item a roster item.
+ */
+ public void addRosterItem(RosterEntry rosterEntry) {
+ RosterGroup rosterGroup = null;
+ // Create a new Item based on the rosterEntry and add it to the packet
+ Item item = new Item(rosterEntry.getUser(), rosterEntry.getName());
+ // Add the entry groups to the item
+ for (Iterator groups = rosterEntry.getGroups(); groups.hasNext(); ) {
+ rosterGroup = (RosterGroup) groups.next();
+ item.addGroupName(rosterGroup.getName());
+ }
+ addRosterItem(item);
+ }
+
+ /**
+ * Adds a roster item to the packet.
+ *
+ * @param item a roster item.
+ */
+ public void addRosterItem(Item item) {
+ synchronized (rosterItems) {
+ rosterItems.add(item);
+ }
+ }
+ /**
+ * Returns the XML element name of the extension sub-packet root element.
+ * Always returns "x"
+ *
+ * @return the XML element name of the packet extension.
+ */
+ public String getElementName() {
+ return "x";
+ }
+
+ /**
+ * Returns the XML namespace of the extension sub-packet root element.
+ * According the specification the namespace is always "jabber:x:roster"
+ * (which is not to be confused with the 'jabber:iq:roster' namespace
+ *
+ * @return the XML namespace of the packet extension.
+ */
+ public String getNamespace() {
+ return "jabber:x:roster";
+ }
+
+ /**
+ * Returns an Iterator for the roster items in the packet.
+ *
+ * @return and Iterator for the roster items in the packet.
+ */
+ public Iterator getRosterItems() {
+ synchronized (rosterItems) {
+ List entries =
+ Collections.unmodifiableList(new ArrayList(rosterItems));
+ return entries.iterator();
+ }
+ }
+
+ /**
+ * Returns the XML representation of a Roster Item Exchange according the specification
+ * Usually the XML representation will be inside of a Message XML representation like
+ * in the following example:
+ *
+ * Any subject you want
+ * This message contains roster items.
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\">");
+ // Loop through all roster items and append them to the string buffer
+ for (Iterator i = getRosterItems(); i.hasNext();) {
+ Item entry = (Item) i.next();
+ buf.append(entry.toXML());
+ }
+ buf.append("").append(getElementName()).append(">");
+ return buf.toString();
+ }
+
+ /**
+ * A roster item, which consists of a JID and , their name and
+ * the groups the roster item belongs to.
+ */
+ public static class Item {
+
+ private String user;
+ private String name;
+ private List groupNames;
+
+ /**
+ * Creates a new roster item.
+ *
+ * @param user the user.
+ * @param name the user's name.
+ */
+ public Item(String user, String name) {
+ this.user = user;
+ this.name = name;
+ groupNames = new ArrayList();
+ }
+
+ /**
+ * Returns the user.
+ *
+ * @return the user.
+ */
+ public String getUser() {
+ return user;
+ }
+
+ /**
+ * Returns the user's name.
+ *
+ * @return the user's name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the user's name.
+ *
+ * @param name the user's name.
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns an Iterator for the group names (as Strings) that the roster item
+ * belongs to.
+ *
+ * @return an Iterator for the group names.
+ */
+ public Iterator getGroupNames() {
+ synchronized (groupNames) {
+ return Collections.unmodifiableList(groupNames).iterator();
+ }
+ }
+
+ /**
+ * Returns a String array for the group names that the roster item
+ * belongs to.
+ *
+ * @return a String[] for the group names.
+ */
+ public String[] getGroupArrayNames() {
+ synchronized (groupNames) {
+ return (String[])(Collections.unmodifiableList(groupNames).toArray(new String[groupNames.size()]));
+ }
+ }
+
+ /**
+ * Adds a group name.
+ *
+ * @param groupName the group name.
+ */
+ public void addGroupName(String groupName) {
+ synchronized (groupNames) {
+ if (!groupNames.contains(groupName)) {
+ groupNames.add(groupName);
+ }
+ }
+ }
+
+ /**
+ * Removes a group name.
+ *
+ * @param groupName the group name.
+ */
+ public void removeGroupName(String groupName) {
+ synchronized (groupNames) {
+ groupNames.remove(groupName);
+ }
+ }
+
+ public String toXML() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("- ");
+ synchronized (groupNames) {
+ for (int i = 0; i < groupNames.size(); i++) {
+ String groupName = (String) groupNames.get(i);
+ buf.append("").append(groupName).append("");
+ }
+ }
+ buf.append("
");
+ return buf.toString();
+ }
+ }
+
+}
diff --git a/source/org/jivesoftware/smackx/provider/RosterExchangeProvider.java b/source/org/jivesoftware/smackx/provider/RosterExchangeProvider.java
new file mode 100644
index 000000000..fee2c0ef1
--- /dev/null
+++ b/source/org/jivesoftware/smackx/provider/RosterExchangeProvider.java
@@ -0,0 +1,67 @@
+/*
+ * Created on 27/07/2003
+ *
+ */
+package org.jivesoftware.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.packet.*;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * The RosterExchangeProvider parses RosterExchange packets.
+ *
+ * @author Gaston Dombiak
+ */
+public class RosterExchangeProvider implements PacketExtensionProvider {
+
+ /**
+ * Creates a new RosterExchangeProvider.
+ * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor
+ */
+ public RosterExchangeProvider() {
+ }
+
+ /**
+ * Parses a RosterExchange packet (extension sub-packet).
+ *
+ * @param parser the XML parser, positioned at the starting element of the extension.
+ * @return a PacketExtension.
+ * @throws Exception if a parsing error occurs.
+ */
+ public PacketExtension parseExtension(XmlPullParser parser)
+ throws Exception {
+
+ RosterExchange rosterExchange = new RosterExchange();
+ boolean done = false;
+ RosterExchange.Item item = null;
+ while (!done) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.START_TAG) {
+ if (parser.getName().equals("item")) {
+ String jid = parser.getAttributeValue("", "jid");
+ String name = parser.getAttributeValue("", "name");
+ // Create packet.
+ item = new RosterExchange.Item(jid, name);
+ }
+ if (parser.getName().equals("group")) {
+ String groupName = parser.nextText();
+ item.addGroupName(groupName);
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ if (parser.getName().equals("item")) {
+ rosterExchange.addRosterItem(item);
+ }
+ if (parser.getName().equals("x")) {
+ done = true;
+ }
+ }
+ }
+
+ return rosterExchange;
+
+ }
+
+}
diff --git a/test/org/jivesoftware/smackx/packet/RosterExchangeTest.java b/test/org/jivesoftware/smackx/packet/RosterExchangeTest.java
new file mode 100644
index 000000000..981db2c93
--- /dev/null
+++ b/test/org/jivesoftware/smackx/packet/RosterExchangeTest.java
@@ -0,0 +1,210 @@
+/*
+ * Created on 01/08/2003
+ *
+ */
+package org.jivesoftware.smackx.packet;
+
+import java.util.Iterator;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.*;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smackx.packet.RosterExchange;
+
+import junit.framework.TestCase;
+
+/**
+ *
+ * Test the Roster Exchange extension
+ *
+ * @author Gaston Dombiak
+ */
+public class RosterExchangeTest extends TestCase {
+
+ /**
+ * Constructor for RosterExchangeTest.
+ * @param arg0
+ */
+ public RosterExchangeTest(String arg0) {
+ super(arg0);
+ }
+
+ /**
+ * 1. User_1 will send his/her roster entries to user_2
+ * 2. User_2 will receives the entries and iterate over them to check if everything is fine
+ * 3. User_1 will wait several seconds for an ACK from user_2, if none is received then something is wrong
+ */
+ public void testSendAndReceiveRosterEntries() {
+ String host = "localhost";
+ String server_user1 = "gato3";
+ String user1 = "gato3@gato.home";
+ String pass1 = "gato3";
+
+ String server_user2 = "gato1";
+ String user2 = "gato1@gato.home";
+ String pass2 = "gato1";
+
+ XMPPConnection conn1 = null;
+ XMPPConnection conn2 = null;
+
+ try {
+ // Connect to the server and log in the users
+ conn1 = new XMPPConnection(host);
+ conn1.login(server_user1, pass1);
+ conn2 = new XMPPConnection(host);
+ conn2.login(server_user2, pass2);
+
+ // Create a chat for each connection
+ Chat chat1 = conn1.createChat(user2);
+ final Chat chat2 = new Chat(conn2, user1, chat1.getChatID());
+
+ // Create a Listener that listens for Messages with the extension "jabber:x:roster"
+ // This listener will listen on the conn2 and answer an ACK if everything is ok
+ PacketFilter packetFilter = new PacketExtensionFilter("x", "jabber:x:roster");
+ PacketListener packetListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ Message message = (Message) packet;
+ assertNotNull("Body is null", message.getBody());
+ try {
+ RosterExchange rosterExchange = (RosterExchange) message.getExtension("x","jabber:x:roster");
+ assertNotNull("Message without extension \"jabber:x:roster\"", rosterExchange);
+ assertTrue("Roster without entries", rosterExchange.getRosterItems().hasNext());
+ for (Iterator it = rosterExchange.getRosterItems(); it.hasNext();) {
+ RosterExchange.Item item = (RosterExchange.Item) it.next();
+ }
+ }
+ catch (ClassCastException e){
+ fail("ClassCastException - Most probable cause is that smack providers is misconfigured");
+ }
+ try {
+ chat2.sendMessage("ok");
+ } catch (Exception e) {
+ fail("An error occured sending ack " + e.getMessage());
+ }
+ }
+ };
+ conn2.addPacketListener(packetListener, packetFilter);
+
+ // Create the message to send with the roster
+ Message msg = new Message(user2);
+ msg.setSubject("Any subject you want");
+ msg.setBody("This message contains roster items.");
+ // Create a RosterExchange Package and add it to the message
+ RosterExchange rosterExchange = new RosterExchange(conn1.getRoster());
+ msg.addExtension(rosterExchange);
+
+ // Send the message that contains the roster
+ try {
+ chat1.sendMessage(msg);
+ } catch (Exception e) {
+ fail("An error occured sending the message with the roster");
+ }
+ // Wait for 2 seconds for a reply
+ msg = chat1.nextMessage(2000);
+ assertNotNull("No reply received", msg);
+ }
+ catch (Exception e) {
+ fail(e.toString());
+ }
+ finally {
+ if (conn1 != null)
+ conn1.close();
+ if (conn2 != null)
+ conn2.close();
+ }
+
+ }
+
+ /**
+ * 1. User_1 will send his/her roster entries to user_2
+ * 2. User_2 will automatically add the entries that receives to his/her roster in the corresponding group
+ * 3. User_1 will wait several seconds for an ACK from user_2, if none is received then something is wrong
+ */
+ public void testSendAndAcceptRosterEntries() {
+ String host = "localhost";
+ String server_user1 = "gato3";
+ String user1 = "gato3@gato.home";
+ String pass1 = "gato3";
+
+ String server_user2 = "gato4";
+ String user2 = "gato4@gato.home";
+ String pass2 = "gato4";
+
+ XMPPConnection conn1 = null;
+ XMPPConnection conn2 = null;
+
+ try {
+ // Connect to the server and log in the users
+ conn1 = new XMPPConnection(host);
+ conn1.login(server_user1, pass1);
+ conn2 = new XMPPConnection(host);
+ conn2.login(server_user2, pass2);
+ final Roster user2_roster = conn2.getRoster();
+
+ // Create a chat for each connection
+ Chat chat1 = conn1.createChat(user2);
+ final Chat chat2 = new Chat(conn2, user1, chat1.getChatID());
+
+ // Create a Listener that listens for Messages with the extension "jabber:x:roster"
+ // This listener will listen on the conn2, save the roster entries and answer an ACK if everything is ok
+ PacketFilter packetFilter = new PacketExtensionFilter("x", "jabber:x:roster");
+ PacketListener packetListener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ Message message = (Message) packet;
+ assertNotNull("Body is null", message.getBody());
+ try {
+ RosterExchange rosterExchange = (RosterExchange) message.getExtension("x","jabber:x:roster");
+ assertNotNull("Message without extension \"jabber:x:roster\"", rosterExchange);
+ assertTrue("Roster without entries", rosterExchange.getRosterItems().hasNext());
+ // Add the roster entries to user2's roster
+ for (Iterator it = rosterExchange.getRosterItems(); it.hasNext();) {
+ RosterExchange.Item item = (RosterExchange.Item) it.next();
+ user2_roster.createEntry(item.getUser(), item.getName(), item.getGroupArrayNames());
+ }
+ }
+ catch (ClassCastException e){
+ fail("ClassCastException - Most probable cause is that smack providers is misconfigured");
+ }
+ catch (Exception e) {
+ fail(e.toString());
+ }
+ try {
+ chat2.sendMessage("ok");
+ } catch (Exception e) {
+ fail("An error occured sending ack " + e.getMessage());
+ }
+ }
+ };
+ conn2.addPacketListener(packetListener, packetFilter);
+
+ // Create the message to send with the roster
+ Message msg = new Message(user2);
+ msg.setSubject("Any subject you want");
+ msg.setBody("This message contains roster items.");
+ // Create a RosterExchange Package and add it to the message
+ RosterExchange rosterExchange = new RosterExchange(conn1.getRoster());
+ msg.addExtension(rosterExchange);
+
+ // Send the message that contains the roster
+ try {
+ chat1.sendMessage(msg);
+ } catch (Exception e) {
+ fail("An error occured sending the message with the roster");
+ }
+ // Wait for 10 seconds for a reply
+ msg = chat1.nextMessage(5000);
+ assertNotNull("No reply received", msg);
+ }
+ catch (Exception e) {
+ fail(e.toString());
+ }
+ finally {
+ if (conn1 != null)
+ conn1.close();
+ if (conn2 != null)
+ conn2.close();
+ }
+
+ }
+
+}