();
+
+ // Delete all entries which where not added or updated
+ for (RosterEntry entry : entries.values()) {
+ toDelete.add(entry.getUser());
+ }
+ toDelete.removeAll(addedEntries);
+ toDelete.removeAll(updatedEntries);
+ for (String user : toDelete) {
+ deleteEntry(deletedEntries, entries.get(user));
+ }
+
+ if (rosterStore != null) {
+ rosterStore.resetEntries(items, version);
+ }
+ }
+
+ /* Ignore ItemTypes as of RFC 6121, 2.1.2.5. */
+ private boolean hasValidSubscriptionType(RosterPacket.Item item) {
+ return item.getItemType().equals(RosterPacket.ItemType.none)
+ || item.getItemType().equals(RosterPacket.ItemType.from)
+ || item.getItemType().equals(RosterPacket.ItemType.to)
+ || item.getItemType().equals(RosterPacket.ItemType.both);
+ }
+
}
+
}
diff --git a/source/org/jivesoftware/smack/RosterEntry.java b/source/org/jivesoftware/smack/RosterEntry.java
index 55b394ed7..3796c3ed6 100644
--- a/source/org/jivesoftware/smack/RosterEntry.java
+++ b/source/org/jivesoftware/smack/RosterEntry.java
@@ -169,6 +169,11 @@ public class RosterEntry {
return buf.toString();
}
+ @Override
+ public int hashCode() {
+ return (user == null ? 0 : user.hashCode());
+ }
+
public boolean equals(Object object) {
if (this == object) {
return true;
@@ -181,11 +186,6 @@ public class RosterEntry {
}
}
- @Override
- public int hashCode() {
- return this.user.hashCode();
- }
-
/**
* Indicates whether some other object is "equal to" this by comparing all members.
*
diff --git a/source/org/jivesoftware/smack/RosterGroup.java b/source/org/jivesoftware/smack/RosterGroup.java
index 80e6438bf..dfd326461 100644
--- a/source/org/jivesoftware/smack/RosterGroup.java
+++ b/source/org/jivesoftware/smack/RosterGroup.java
@@ -28,7 +28,8 @@ import org.jivesoftware.smack.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.List;
+import java.util.LinkedHashSet;
+import java.util.Set;
/**
* A group of roster entries.
@@ -40,7 +41,7 @@ public class RosterGroup {
private String name;
private Connection connection;
- private final List entries;
+ private final Set entries;
/**
* Creates a new roster group instance.
@@ -51,7 +52,7 @@ public class RosterGroup {
RosterGroup(String name, Connection connection) {
this.name = name;
this.connection = connection;
- entries = new ArrayList();
+ entries = new LinkedHashSet();
}
/**
@@ -235,7 +236,7 @@ public class RosterGroup {
}
void addEntryLocal(RosterEntry entry) {
- // Only add the entry if it isn't already in the list.
+ // Update the entry if it is already in the list
synchronized (entries) {
entries.remove(entry);
entries.add(entry);
diff --git a/source/org/jivesoftware/smack/RosterStore.java b/source/org/jivesoftware/smack/RosterStore.java
new file mode 100644
index 000000000..d6627b95d
--- /dev/null
+++ b/source/org/jivesoftware/smack/RosterStore.java
@@ -0,0 +1,67 @@
+/**
+ * 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;
+
+import java.util.Collection;
+
+import org.jivesoftware.smack.packet.RosterPacket;
+
+/**
+ * This is an interface for persistent roster store needed to implement
+ * roster versioning as per RFC 6121.
+ */
+
+public interface RosterStore {
+
+ /**
+ * This method returns a collection of all roster items contained in this store.
+ * @return List of {@link RosterEntry}
+ */
+ public Collection getEntries();
+ /**
+ * This method returns the roster item in this store for the given JID.
+ * @param bareJid The bare JID of the RosterEntry
+ * @return The {@link RosterEntry} which belongs to that user
+ */
+ public RosterPacket.Item getEntry(String bareJid);
+ /**
+ * This method returns the version number as specified by the "ver" attribute
+ * of the local store. For a fresh store, this MUST be the empty string.
+ * @return local roster version
+ */
+ public String getRosterVersion();
+ /**
+ * This method stores a new roster entry in this store or updates an existing one.
+ * @param item the entry to store
+ * @param version the new roster version
+ * @return True if successful
+ */
+ public boolean addEntry(RosterPacket.Item item, String version);
+ /**
+ * This method updates the store so that it contains only the given entries.
+ * @param items the entries to store
+ * @param version the new roster version
+ * @return True if successful
+ */
+ public boolean resetEntries(Collection items, String version);
+ /**
+ * Removes an entry from the store
+ * @param bareJid The bare JID of the entry to be removed
+ * @param version the new roster version
+ * @return True if successful
+ */
+ public boolean removeEntry(String bareJid, String version);
+
+}
diff --git a/source/org/jivesoftware/smack/packet/RosterPacket.java b/source/org/jivesoftware/smack/packet/RosterPacket.java
index 9360480af..295d5363d 100644
--- a/source/org/jivesoftware/smack/packet/RosterPacket.java
+++ b/source/org/jivesoftware/smack/packet/RosterPacket.java
@@ -33,6 +33,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
public class RosterPacket extends IQ {
private final List- rosterItems = new ArrayList
- ();
+ private String rosterVersion;
/**
* Adds a roster item to the packet.
@@ -69,7 +70,13 @@ public class RosterPacket extends IQ {
public String getChildElementXML() {
StringBuilder buf = new StringBuilder();
- buf.append("");
+ buf.append("");
synchronized (rosterItems) {
for (Item entry : rosterItems) {
buf.append(entry.toXML());
@@ -79,6 +86,14 @@ public class RosterPacket extends IQ {
return buf.toString();
}
+ public String getVersion() {
+ return rosterVersion;
+ }
+
+ public void setVersion(String version) {
+ rosterVersion = version;
+ }
+
/**
* A roster item, which consists of a JID, their name, the type of subscription, and
* the groups the roster item belongs to.
@@ -198,7 +213,7 @@ public class RosterPacket extends IQ {
public String toXML() {
StringBuilder buf = new StringBuilder();
- buf.append("
- ");
return buf.toString();
}
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((groupNames == null) ? 0 : groupNames.hashCode());
+ result = prime * result + ((itemStatus == null) ? 0 : itemStatus.hashCode());
+ result = prime * result + ((itemType == null) ? 0 : itemType.hashCode());
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ result = prime * result + ((user == null) ? 0 : user.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ Item other = (Item) obj;
+ if (groupNames == null) {
+ if (other.groupNames != null)
+ return false;
+ }
+ else if (!groupNames.equals(other.groupNames))
+ return false;
+ if (itemStatus != other.itemStatus)
+ return false;
+ if (itemType != other.itemType)
+ return false;
+ if (name == null) {
+ if (other.name != null)
+ return false;
+ }
+ else if (!name.equals(other.name))
+ return false;
+ if (user == null) {
+ if (other.user != null)
+ return false;
+ }
+ else if (!user.equals(other.user))
+ return false;
+ return true;
+ }
+
}
/**
* The subscription status of a roster item. An optional element that indicates
* the subscription status if a change request is pending.
*/
- public static class ItemStatus {
+ public static enum ItemStatus {
+ /**
+ * Request to subscribe
+ */
+ subscribe,
/**
- * Request to subcribe.
+ * Request to unsubscribe
*/
- public static final ItemStatus SUBSCRIPTION_PENDING = new ItemStatus("subscribe");
+ unsubscribe;
- /**
- * Request to unsubscribe.
- */
- public static final ItemStatus UNSUBSCRIPTION_PENDING = new ItemStatus("unsubscribe");
+ public static final ItemStatus SUBSCRIPTION_PENDING = subscribe;
+ public static final ItemStatus UNSUBSCRIPTION_PENDING = unsubscribe;
- public static ItemStatus fromString(String value) {
- if (value == null) {
+ public static ItemStatus fromString(String s) {
+ if (s == null) {
return null;
}
- value = value.toLowerCase();
- if ("unsubscribe".equals(value)) {
- return UNSUBSCRIPTION_PENDING;
+ try {
+ return ItemStatus.valueOf(s);
}
- else if ("subscribe".equals(value)) {
- return SUBSCRIPTION_PENDING;
- }
- else {
+ catch (IllegalArgumentException e) {
return null;
}
}
-
- private String value;
-
- /**
- * Returns the item status associated with the specified string.
- *
- * @param value the item status.
- */
- private ItemStatus(String value) {
- this.value = value;
- }
-
- public String toString() {
- return value;
- }
}
public static enum ItemType {
diff --git a/source/org/jivesoftware/smack/util/PacketParserUtils.java b/source/org/jivesoftware/smack/util/PacketParserUtils.java
index a8d6b9e36..44bac60b8 100644
--- a/source/org/jivesoftware/smack/util/PacketParserUtils.java
+++ b/source/org/jivesoftware/smack/util/PacketParserUtils.java
@@ -419,6 +419,10 @@ public class PacketParserUtils {
RosterPacket roster = new RosterPacket();
boolean done = false;
RosterPacket.Item item = null;
+
+ String version = parser.getAttributeValue("", "ver");
+ roster.setVersion(version);
+
while (!done) {
int eventType = parser.next();
if (eventType == XmlPullParser.START_TAG) {
@@ -436,7 +440,7 @@ public class PacketParserUtils {
RosterPacket.ItemType type = RosterPacket.ItemType.valueOf(subscription != null ? subscription : "none");
item.setItemType(type);
}
- if (parser.getName().equals("group") && item!= null) {
+ else if (parser.getName().equals("group") && item!= null) {
final String groupName = parser.nextText();
if (groupName != null && groupName.trim().length() > 0) {
item.addGroupName(groupName);
diff --git a/source/org/jivesoftware/smack/util/StringUtils.java b/source/org/jivesoftware/smack/util/StringUtils.java
index eecd84dda..915c98082 100644
--- a/source/org/jivesoftware/smack/util/StringUtils.java
+++ b/source/org/jivesoftware/smack/util/StringUtils.java
@@ -520,10 +520,52 @@ public class StringUtils {
return buf.toString();
}
+ /**
+ * Encodes a string for use in an XML attribute by escaping characters with
+ * a special meaning. In particular, white spaces are encoded as character
+ * references, such that they are not replaced by ' ' on parsing.
+ */
+ private static String xmlAttribEncodeBinary(String value) {
+ StringBuilder s = new StringBuilder();
+ char buf[] = value.toCharArray();
+ for (char c : buf) {
+ switch (c) {
+ case '<': s.append("<"); break;
+ case '>': s.append(">"); break;
+ case '&': s.append("&"); break;
+ case '"': s.append("""); break;
+ case '\'': s.append("'"); break;
+ default:
+ if (c <= 0x1f || (0x7f <= c && c <= 0x9f)) { // includes \t, \n, \r
+ s.append("");
+ s.append(String.format("%X", (int)c));
+ s.append(';');
+ } else {
+ s.append(c);
+ }
+ }
+ }
+ return s.toString();
+ }
+
+ /**
+ * Returns a string representing a XML attribute. The value parameter is escaped as necessary. In particular,
+ * white spaces are encoded as character references, such that they are not replaced by ' ' on parsing.
+ * @param name name of the XML attribute
+ * @param value value of the XML attribute
+ */
+ public static String xmlAttrib(String name, String value) {
+ return name + "=\"" + xmlAttribEncodeBinary(value) + "\"";
+ }
+
+
/**
* Escapes all necessary characters in the String so that it can be used
* in an XML doc.
*
+ * Warning: This method does not escape unicode character references
+ * (i.e. references of the from ë)
+ *
* @param string the string to escape.
* @return the string with appropriate characters escaped.
*/
diff --git a/test-unit/org/jivesoftware/smack/DefaultRosterStoreTest.java b/test-unit/org/jivesoftware/smack/DefaultRosterStoreTest.java
new file mode 100644
index 000000000..81dfad2d2
--- /dev/null
+++ b/test-unit/org/jivesoftware/smack/DefaultRosterStoreTest.java
@@ -0,0 +1,222 @@
+/**
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.packet.RosterPacket.Item;
+import org.jivesoftware.smack.packet.RosterPacket.ItemStatus;
+import org.jivesoftware.smack.packet.RosterPacket.ItemType;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Tests the implementation of {@link DefaultRosterStore}.
+ *
+ * @author Lars Noschinski
+ */
+public class DefaultRosterStoreTest {
+
+ @Rule
+ public TemporaryFolder tmpFolder = new TemporaryFolder();
+
+ @Before
+ public void setUp() throws Exception {
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ }
+
+ /**
+ * Tests that opening an uninitialized directory fails.
+ */
+ @Test
+ public void testStoreUninitialized() throws IOException {
+ File storeDir = tmpFolder.newFolder();
+ assertNull(DefaultRosterStore.open(storeDir));
+ }
+
+ /**
+ * Tests that an initialized directory is empty.
+ */
+ @Test
+ public void testStoreInitializedEmpty() throws IOException {
+ File storeDir = tmpFolder.newFolder();
+ DefaultRosterStore store = DefaultRosterStore.init(storeDir);
+ assertNotNull("Initialization returns store", store);
+ assertEquals("Freshly initialized store must have empty version",
+ "", store.getRosterVersion());
+ assertEquals("Freshly initialized store must have no entries",
+ 0, store.getEntries().size());
+ }
+
+ /**
+ * Tests adding and removing entries
+ */
+ @Test
+ public void testStoreAddRemove() throws IOException {
+ File storeDir = tmpFolder.newFolder();
+ DefaultRosterStore store = DefaultRosterStore.init(storeDir);
+
+ assertEquals("Initial roster version", "", store.getRosterVersion());
+
+ String userName = "user@example.com";
+
+ final RosterPacket.Item item1 = new Item(userName, null);
+ final String version1 = "1";
+ store.addEntry(item1, version1);
+
+ assertEquals("Adding entry sets version correctly", version1, store.getRosterVersion());
+
+ RosterPacket.Item storedItem = store.getEntry(userName);
+ assertNotNull("Added entry not found found", storedItem);
+ assertEquals("User of added entry",
+ item1.getUser(), storedItem.getUser());
+ assertEquals("Name of added entry",
+ item1.getName(), storedItem.getName());
+ assertEquals("Groups", item1.getGroupNames(), storedItem.getGroupNames());
+ assertEquals("ItemType of added entry",
+ item1.getItemType(), storedItem.getItemType());
+ assertEquals("ItemStatus of added entry",
+ item1.getItemStatus(), storedItem.getItemStatus());
+
+
+ final String version2 = "2";
+ final RosterPacket.Item item2 = new Item(userName, "Ursula Example");
+ item2.addGroupName("users");
+ item2.addGroupName("examples");
+ item2.setItemStatus(ItemStatus.subscribe);
+ item2.setItemType(ItemType.none);
+ store.addEntry(item2,version2);
+ assertEquals("Updating entry sets version correctly", version2, store.getRosterVersion());
+ storedItem = store.getEntry(userName);
+ assertNotNull("Added entry not found", storedItem);
+ assertEquals("User of added entry",
+ item2.getUser(), storedItem.getUser());
+ assertEquals("Name of added entry",
+ item2.getName(), storedItem.getName());
+ assertEquals("Groups", item2.getGroupNames(), storedItem.getGroupNames());
+ assertEquals("ItemType of added entry",
+ item2.getItemType(), storedItem.getItemType());
+ assertEquals("ItemStatus of added entry",
+ item2.getItemStatus(), storedItem.getItemStatus());
+
+ List
- entries = store.getEntries();
+ assertEquals("Number of entries", 1, entries.size());
+
+ final RosterPacket.Item item3 = new Item("foobar@example.com", "Foo Bar");
+ item3.addGroupName("The Foo Fighters");
+ item3.addGroupName("Bar Friends");
+ item3.setItemStatus(ItemStatus.unsubscribe);
+ item3.setItemType(ItemType.both);
+
+ final RosterPacket.Item item4 = new Item("baz@example.com", "Baba Baz");
+ item4.addGroupName("The Foo Fighters");
+ item4.addGroupName("Bar Friends");
+ item4.setItemStatus(ItemStatus.subscribe);
+ item4.setItemType(ItemType.both);
+
+ ArrayList
- items34 = new ArrayList();
+ items34.add(item3);
+ items34.add(item4);
+
+ String version3 = "3";
+ store.resetEntries(items34, version3);
+
+ storedItem = store.getEntry("foobar@example.com");
+ assertNotNull("Added entry not found", storedItem);
+ assertEquals("User of added entry",
+ item3.getUser(), storedItem.getUser());
+ assertEquals("Name of added entry",
+ item3.getName(), storedItem.getName());
+ assertEquals("Groups", item3.getGroupNames(), storedItem.getGroupNames());
+ assertEquals("ItemType of added entry",
+ item3.getItemType(), storedItem.getItemType());
+ assertEquals("ItemStatus of added entry",
+ item3.getItemStatus(), storedItem.getItemStatus());
+
+
+ storedItem = store.getEntry("baz@example.com");
+ assertNotNull("Added entry not found", storedItem);
+ assertEquals("User of added entry",
+ item4.getUser(), storedItem.getUser());
+ assertEquals("Name of added entry",
+ item4.getName(), storedItem.getName());
+ assertEquals("Groups", item4.getGroupNames(), storedItem.getGroupNames());
+ assertEquals("ItemType of added entry",
+ item4.getItemType(), storedItem.getItemType());
+ assertEquals("ItemStatus of added entry",
+ item4.getItemStatus(), storedItem.getItemStatus());
+
+ entries = store.getEntries();
+ assertEquals("Number of entries", 2, entries.size());
+
+ String version4 = "4";
+ store.removeEntry("baz@example.com", version4);
+ assertEquals("Removing entry sets version correctly",
+ version4, store.getRosterVersion());
+ assertNull("Removed entry is gone", store.getEntry(userName));
+
+ entries = store.getEntries();
+ assertEquals("Number of entries", 1, entries.size());
+ }
+
+ /**
+ * Tests adding entries with evil characters
+ */
+ @Test
+ public void testAddEvilChars() throws IOException {
+ File storeDir = tmpFolder.newFolder();
+ DefaultRosterStore store = DefaultRosterStore.init(storeDir);
+
+ String user = "../_#;\"'\\&@example.com";
+ String name = "\n../_#\0\t;\"'&@\\";
+ String group1 = "\t;\"'&@\\\n../_#\0";
+ String group2 = "#\0\t;\"'&@\\\n../_";
+
+ Item item = new Item(user, name);
+ item.setItemStatus(ItemStatus.unsubscribe);
+ item.setItemType(ItemType.to);
+ item.addGroupName(group1);
+ item.addGroupName(group2);
+ store.addEntry(item, "a-version");
+ Item storedItem = store.getEntry(user);
+
+ assertNotNull("Added entry not found", storedItem);
+ assertEquals("User of added entry",
+ item.getUser(), storedItem.getUser());
+ assertEquals("Name of added entry",
+ item.getName(), storedItem.getName());
+ assertEquals("Groups", item.getGroupNames(), storedItem.getGroupNames());
+ assertEquals("ItemType of added entry",
+ item.getItemType(), storedItem.getItemType());
+ assertEquals("ItemStatus of added entry",
+ item.getItemStatus(), storedItem.getItemStatus());
+
+ }
+}
+
diff --git a/test-unit/org/jivesoftware/smack/RosterTest.java b/test-unit/org/jivesoftware/smack/RosterTest.java
index c65a213ec..f2e9f1d86 100644
--- a/test-unit/org/jivesoftware/smack/RosterTest.java
+++ b/test-unit/org/jivesoftware/smack/RosterTest.java
@@ -355,6 +355,25 @@ public class RosterTest {
assertSame("Wrong number of roster entries.", 4, roster.getEntries().size());
}
+ /**
+ * Tests that roster pushes with invalid from are ignored.
+ *
+ * @see RFC 6121, Section 2.1.6
+ */
+ @Test(timeout=5000)
+ public void testIgnoreInvalidFrom() {
+ RosterPacket packet = new RosterPacket();
+ packet.setType(Type.SET);
+ packet.setTo(connection.getUser());
+ packet.setFrom("mallory@example.com");
+ packet.addRosterItem(new Item("spam@example.com", "Cool products!"));
+
+ // Simulate receiving the roster push
+ connection.processPacket(packet);
+
+ assertNull("Contact was added to roster", connection.getRoster().getEntry("spam@example.com"));
+ }
+
/**
* Test if adding an user with an empty group is equivalent with providing
* no group.
diff --git a/test-unit/org/jivesoftware/smack/RosterVersioningTest.java b/test-unit/org/jivesoftware/smack/RosterVersioningTest.java
new file mode 100644
index 000000000..797c4b2d9
--- /dev/null
+++ b/test-unit/org/jivesoftware/smack/RosterVersioningTest.java
@@ -0,0 +1,242 @@
+/**
+ * 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;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashSet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.IQ.Type;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.packet.RosterPacket.Item;
+import org.jivesoftware.smack.packet.RosterPacket.ItemType;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+/**
+ * Tests that verify the correct behavior of the {@see Roster} implementation
+ * with regard to roster versioning
+ *
+ * @see Roster
+ * @see Managing the Roster
+ * @author Fabian Schuetz
+ * @author Lars Noschinski
+ */
+public class RosterVersioningTest {
+
+ private DummyConnection connection;
+
+ @Rule
+ public TemporaryFolder tmpFolder = new TemporaryFolder();
+
+ @Before
+ public void setUp() throws Exception {
+ // Uncomment this to enable debug output
+ //Connection.DEBUG_ENABLED = true;
+
+ DefaultRosterStore store = DefaultRosterStore.init(tmpFolder.newFolder("store"));
+ populateStore(store);
+
+ ConnectionConfiguration conf = new ConnectionConfiguration("dummy");
+ conf.setRosterStore(store);
+ connection = new DummyConnection(conf);
+ connection.connect();
+
+ connection.setRosterVersioningSupported();
+
+ connection.login("rostertest", "secret");
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (connection != null) {
+ connection.disconnect();
+ connection = null;
+ }
+ }
+
+ /**
+ * Tests that receiving an empty roster result causes the roster to be populated
+ * by all entries of the roster store.
+ */
+ @Test(timeout = 5000)
+ public void testEqualVersionStored() throws InterruptedException, IOException {
+ connection.getRoster().reload();
+ answerWithEmptyRosterResult();
+
+ Roster roster = connection.getRoster();
+ Collection entries = roster.getEntries();
+ assertSame("Size of the roster", 3, entries.size());
+
+ HashSet
- items = new HashSet
- ();
+ for (RosterEntry entry : entries) {
+ items.add(RosterEntry.toRosterItem(entry));
+ }
+ RosterStore store = DefaultRosterStore.init(tmpFolder.newFolder());
+ populateStore(store);
+ assertEquals("Elements of the roster", new HashSet
- (store.getEntries()), items);
+
+ for (RosterEntry entry : entries) {
+ assertTrue("joe stevens".equals(entry.getName()) || "geoff hurley".equals(entry.getName())
+ || "higgins mcmann".equals(entry.getName()));
+ }
+ Collection groups = roster.getGroups();
+ assertSame(3, groups.size());
+
+ for (RosterGroup group : groups) {
+ assertTrue("all".equals(group.getName()) || "friends".equals(group.getName())
+ || "partners".equals(group.getName()));
+ }
+ }
+
+ /**
+ * Tests that a non-empty roster result empties the store.
+ */
+ @Test(timeout = 5000)
+ public void testOtherVersionStored() throws InterruptedException {
+ connection.getRoster().reload();
+
+ Item vaglafItem = vaglafItem();
+
+ // We expect that the roster request is the only packet sent. This is not part of the specification,
+ // but a shortcut in the test implementation.
+ Packet sentPacket = connection.getSentPacket();
+ if (sentPacket instanceof RosterPacket) {
+ RosterPacket sentRP = (RosterPacket)sentPacket;
+ RosterPacket answer = new RosterPacket();
+ answer.setPacketID(sentRP.getPacketID());
+ answer.setType(Type.RESULT);
+ answer.setTo(sentRP.getFrom());
+
+ answer.setVersion("newVersion");
+ answer.addRosterItem(vaglafItem);
+
+ connection.processPacket(answer);
+ } else {
+ assertTrue("Expected to get a RosterPacket ", false);
+ }
+
+ Roster roster = connection.getRoster();
+ assertEquals("Size of roster", 1, roster.getEntries().size());
+ RosterEntry entry = roster.getEntry(vaglafItem.getUser());
+ assertNotNull("Roster contains vaglaf entry", entry);
+ assertEquals("vaglaf entry in roster equals the sent entry", vaglafItem, RosterEntry.toRosterItem(entry));
+
+ RosterStore store = connection.getConfiguration().getRosterStore();
+ assertEquals("Size of store", 1, store.getEntries().size());
+ Item item = store.getEntry(vaglafItem.getUser());
+ assertNotNull("Store contains vaglaf entry");
+ assertEquals("vaglaf entry in store equals the sent entry", vaglafItem, item);
+ }
+
+ /**
+ * Test roster versioning with roster pushes
+ */
+ @Test(timeout = 5000)
+ public void testRosterVersioningWithCachedRosterAndPushes() throws Throwable {
+ connection.getRoster().reload();
+ answerWithEmptyRosterResult();
+
+ RosterStore store = connection.getConfiguration().getRosterStore();
+ Roster roster = connection.getRoster();
+
+ // Simulate a roster push adding vaglaf
+ {
+ RosterPacket rosterPush = new RosterPacket();
+ rosterPush.setTo("rostertest@example.com/home");
+ rosterPush.setType(Type.SET);
+ rosterPush.setVersion("v97");
+
+ Item pushedItem = vaglafItem();
+ rosterPush.addRosterItem(pushedItem);
+ connection.processPacket(rosterPush);
+
+ assertEquals("Expect store version after push", "v97", store.getRosterVersion());
+
+ Item storedItem = store.getEntry("vaglaf@example.com");
+ assertNotNull("Expect vaglaf to be added", storedItem);
+ assertEquals("Expect vaglaf to be equal to pushed item", pushedItem, storedItem);
+
+ Collection
- rosterItems = new HashSet
- ();
+ for (RosterEntry entry : roster.getEntries()) {
+ rosterItems.add(RosterEntry.toRosterItem(entry));
+ }
+ assertEquals(rosterItems, new HashSet
- (store.getEntries()));
+ }
+
+ // Simulate a roster push removing vaglaf
+ {
+ RosterPacket rosterPush = new RosterPacket();
+ rosterPush.setTo("rostertest@example.com/home");
+ rosterPush.setType(Type.SET);
+ rosterPush.setVersion("v98");
+
+ Item item = new Item("vaglaf@example.com", "vaglaf the only");
+ item.setItemType(ItemType.remove);
+ rosterPush.addRosterItem(item);
+ connection.processPacket(rosterPush);
+
+ assertNull("Store doses not contain vaglaf", store.getEntry("vaglaf@example.com"));
+ assertEquals("Expect store version after push", "v98", store.getRosterVersion());
+ }
+ }
+
+ private Item vaglafItem() {
+ Item item = new Item("vaglaf@example.com", "vaglaf the only");
+ item.setItemType(ItemType.both);
+ item.addGroupName("all");
+ item.addGroupName("friends");
+ item.addGroupName("partners");
+ return item;
+ }
+
+ private void populateStore(RosterStore store) throws IOException {
+ store.addEntry(new RosterPacket.Item("geoff@example.com", "geoff hurley"), "");
+
+ RosterPacket.Item item = new RosterPacket.Item("joe@example.com", "joe stevens");
+ item.addGroupName("friends");
+ item.addGroupName("partners");
+ store.addEntry(item, "");
+
+ item = new RosterPacket.Item("higgins@example.com", "higgins mcmann");
+ item.addGroupName("all");
+ item.addGroupName("friends");
+ store.addEntry(item, "v96");
+ }
+
+ private void answerWithEmptyRosterResult() throws InterruptedException {
+ // We expect that the roster request is the only packet sent. This is not part of the specification,
+ // but a shortcut in the test implementation.
+ Packet sentPacket = connection.getSentPacket();
+ if (sentPacket instanceof RosterPacket) {
+ final IQ emptyIQ = IQ.createResultIQ((RosterPacket)sentPacket);
+ connection.processPacket(emptyIQ);
+ } else {
+ assertTrue("Expected to get a RosterPacket ", false);
+ }
+ }
+
+}