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