mirror of
https://codeberg.org/Mercury-IM/Smack
synced 2024-11-26 16:22:06 +01:00
Implement support for roster versioning
Roster versioning is defined in RFC 6121, section 2.2.6; the protocol was originally described in XEP-0237. Fixes SMACK-399
This commit is contained in:
parent
e7a2cad401
commit
c06b0a7720
14 changed files with 1300 additions and 147 deletions
|
@ -211,6 +211,11 @@ public abstract class Connection {
|
|||
*/
|
||||
private String serviceCapsNode;
|
||||
|
||||
/**
|
||||
* Stores whether the server supports rosterVersioning
|
||||
*/
|
||||
private boolean rosterVersioningSupported = false;
|
||||
|
||||
protected XMPPInputOutputStream compressionHandler;
|
||||
|
||||
/**
|
||||
|
@ -829,6 +834,22 @@ public abstract class Connection {
|
|||
return serviceCapsNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the server supports roster versioning as defined in XEP-0237.
|
||||
*
|
||||
* @return true if the server supports roster versioning
|
||||
*/
|
||||
public boolean isRosterVersioningSupported() {
|
||||
return rosterVersioningSupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that the server supports roster versioning as defined in XEP-0237.
|
||||
*/
|
||||
protected void setRosterVersioningSupported() {
|
||||
rosterVersioningSupported = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper class to associate a packet filter with a listener.
|
||||
*/
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.jivesoftware.smack.util.dns.HostAddress;
|
|||
import javax.net.SocketFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@ -90,8 +91,13 @@ public class ConnectionConfiguration implements Cloneable {
|
|||
private boolean sendPresence = true;
|
||||
private boolean rosterLoadedAtLogin = true;
|
||||
private SecurityMode securityMode = SecurityMode.enabled;
|
||||
|
||||
// Holds the proxy information (such as proxyhost, proxyport, username, password etc)
|
||||
|
||||
/**
|
||||
* Permanent store for the Roster, needed for roster versioning
|
||||
*/
|
||||
private RosterStore rosterStore;
|
||||
|
||||
// Holds the proxy information (such as proxyhost, proxyport, username, password etc)
|
||||
protected ProxyInfo proxy;
|
||||
|
||||
/**
|
||||
|
@ -696,6 +702,21 @@ public class ConnectionConfiguration implements Cloneable {
|
|||
return Collections.unmodifiableList(hostAddresses);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the permanent roster store
|
||||
*/
|
||||
public void setRosterStore(RosterStore store) {
|
||||
rosterStore = store;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permanent roster store
|
||||
*/
|
||||
public RosterStore getRosterStore() {
|
||||
return rosterStore;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An enumeration for TLS security modes that are available when making a connection
|
||||
* to the XMPP server.
|
||||
|
|
351
source/org/jivesoftware/smack/DefaultRosterStore.java
Normal file
351
source/org/jivesoftware/smack/DefaultRosterStore.java
Normal file
|
@ -0,0 +1,351 @@
|
|||
/**
|
||||
* 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.io.File;
|
||||
import java.io.FileFilter;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileReader;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.jivesoftware.smack.packet.RosterPacket;
|
||||
import org.jivesoftware.smack.packet.RosterPacket.Item;
|
||||
import org.jivesoftware.smack.util.Base32Encoder;
|
||||
import org.jivesoftware.smack.util.StringUtils;
|
||||
import org.xmlpull.mxp1.MXParser;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
/**
|
||||
* Stores roster entries as specified by RFC 6121 for roster versioning
|
||||
* in a set of files.
|
||||
*
|
||||
* @author Lars Noschinski
|
||||
* @author Fabian Schuetz
|
||||
*/
|
||||
public class DefaultRosterStore implements RosterStore {
|
||||
|
||||
private final File fileDir;
|
||||
|
||||
private static final String ENTRY_PREFIX = "entry-";
|
||||
private static final String VERSION_FILE_NAME = "__version__";
|
||||
private static final String STORE_ID = "DEFAULT_ROSTER_STORE";
|
||||
|
||||
private static final FileFilter rosterDirFilter = new FileFilter() {
|
||||
|
||||
@Override
|
||||
public boolean accept(File file) {
|
||||
String name = file.getName();
|
||||
return name.startsWith(ENTRY_PREFIX);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @param baseDir
|
||||
* will be the directory where all roster entries are stored. One
|
||||
* file for each entry, such that file.name = entry.username.
|
||||
* There is also one special file '__version__' that contains the
|
||||
* current version string.
|
||||
*/
|
||||
private DefaultRosterStore(final File baseDir) {
|
||||
this.fileDir = baseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new roster store on disk
|
||||
*
|
||||
* @param baseDir
|
||||
* The directory to create the store in. The directory should
|
||||
* be empty
|
||||
* @return A {@link DefaultRosterStore} instance if successful,
|
||||
* <code>null</code> else.
|
||||
*/
|
||||
public static DefaultRosterStore init(final File baseDir) {
|
||||
DefaultRosterStore store = new DefaultRosterStore(baseDir);
|
||||
if (store.setRosterVersion("")) {
|
||||
return store;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a roster store
|
||||
* @param baseDir
|
||||
* The directory containing the roster store.
|
||||
* @return A {@link DefaultRosterStore} instance if successful,
|
||||
* <code>null</code> else.
|
||||
*/
|
||||
public static DefaultRosterStore open(final File baseDir) {
|
||||
DefaultRosterStore store = new DefaultRosterStore(baseDir);
|
||||
String s = store.readFile(store.getVersionFile());
|
||||
if (s != null && s.startsWith(STORE_ID + "\n")) {
|
||||
return store;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private File getVersionFile() {
|
||||
return new File(fileDir, VERSION_FILE_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Item> getEntries() {
|
||||
List<Item> entries = new ArrayList<RosterPacket.Item>();
|
||||
|
||||
for (File file : fileDir.listFiles(rosterDirFilter)) {
|
||||
Item entry = readEntry(file);
|
||||
if (entry == null) {
|
||||
log("Roster store file '" + file + "' is invalid.");
|
||||
}
|
||||
else {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Item getEntry(String bareJid) {
|
||||
return readEntry(getBareJidFile(bareJid));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRosterVersion() {
|
||||
String s = readFile(getVersionFile());
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
String[] lines = s.split("\n", 2);
|
||||
if (lines.length < 2) {
|
||||
return null;
|
||||
}
|
||||
return lines[1];
|
||||
}
|
||||
|
||||
private boolean setRosterVersion(String version) {
|
||||
return writeFile(getVersionFile(), STORE_ID + "\n" + version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addEntry(Item item, String version) {
|
||||
return addEntryRaw(item) && setRosterVersion(version);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeEntry(String bareJid, String version) {
|
||||
try {
|
||||
return getBareJidFile(bareJid).delete() && setRosterVersion(version);
|
||||
}
|
||||
catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean resetEntries(Collection<Item> items, String version) {
|
||||
try {
|
||||
for (File file : fileDir.listFiles(rosterDirFilter)) {
|
||||
file.delete();
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
for (Item item : items) {
|
||||
if (!addEntryRaw(item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return setRosterVersion(version);
|
||||
}
|
||||
|
||||
private Item readEntry(File file) {
|
||||
String s = readFile(file);
|
||||
if (s == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String user = null;
|
||||
String name = null;
|
||||
String type = null;
|
||||
String status = null;
|
||||
|
||||
List<String> groupNames = new ArrayList<String>();
|
||||
|
||||
try {
|
||||
XmlPullParser parser = new MXParser();
|
||||
parser.setInput(new StringReader(s));
|
||||
|
||||
boolean done = false;
|
||||
while (!done) {
|
||||
int eventType = parser.next();
|
||||
if (eventType == XmlPullParser.START_TAG) {
|
||||
if (parser.getName().equals("item")) {
|
||||
user = parser.getAttributeValue(null, "user");
|
||||
name = parser.getAttributeValue(null, "name");
|
||||
type = parser.getAttributeValue(null, "type");
|
||||
status = parser.getAttributeValue(null, "status");
|
||||
}
|
||||
if (parser.getName().equals("group")) {
|
||||
String group = parser.getAttributeValue(null, "name");
|
||||
if (group != null) {
|
||||
groupNames.add(group);
|
||||
}
|
||||
else {
|
||||
log("Invalid group entry in store entry file "
|
||||
+ file);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (eventType == XmlPullParser.END_TAG) {
|
||||
if (parser.getName().equals("item")) {
|
||||
done = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();;
|
||||
return null;
|
||||
}
|
||||
catch (XmlPullParserException e) {
|
||||
log("Invalid group entry in store entry file "
|
||||
+ file);
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (user == null) {
|
||||
return null;
|
||||
}
|
||||
RosterPacket.Item item = new RosterPacket.Item(user, name);
|
||||
for (String groupName : groupNames) {
|
||||
item.addGroupName(groupName);
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
try {
|
||||
item.setItemType(RosterPacket.ItemType.valueOf(type));
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
log("Invalid type in store entry file " + file);
|
||||
return null;
|
||||
}
|
||||
if (status != null) {
|
||||
RosterPacket.ItemStatus itemStatus = RosterPacket.ItemStatus
|
||||
.fromString(status);
|
||||
if (itemStatus == null) {
|
||||
log("Invalid status in store entry file " + file);
|
||||
return null;
|
||||
}
|
||||
item.setItemStatus(itemStatus);
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
private boolean addEntryRaw (Item item) {
|
||||
StringBuilder s = new StringBuilder();
|
||||
s.append("<item ");
|
||||
s.append(StringUtils.xmlAttrib("user", item.getUser()));
|
||||
s.append(" ");
|
||||
if (item.getName() != null) {
|
||||
s.append(StringUtils.xmlAttrib("name", item.getName()));
|
||||
s.append(" ");
|
||||
}
|
||||
if (item.getItemType() != null) {
|
||||
s.append(StringUtils.xmlAttrib("type", item.getItemType().name()));
|
||||
s.append(" ");
|
||||
}
|
||||
if (item.getItemStatus() != null) {
|
||||
s.append(StringUtils.xmlAttrib("status", item.getItemStatus().toString()));
|
||||
s.append(" ");
|
||||
}
|
||||
s.append(">");
|
||||
for (String group : item.getGroupNames()) {
|
||||
s.append("<group ");
|
||||
s.append(StringUtils.xmlAttrib("name", group));
|
||||
s.append(" />");
|
||||
}
|
||||
s.append("</item>");
|
||||
return writeFile(getBareJidFile(item.getUser()), s.toString());
|
||||
}
|
||||
|
||||
|
||||
private File getBareJidFile(String bareJid) {
|
||||
String encodedJid = Base32Encoder.getInstance().encode(bareJid);
|
||||
return new File(fileDir, ENTRY_PREFIX + encodedJid);
|
||||
}
|
||||
|
||||
private String readFile(File file) {
|
||||
try {
|
||||
Reader reader = null;
|
||||
try {
|
||||
char buf[] = new char[8192];
|
||||
int len;
|
||||
StringBuilder s = new StringBuilder();
|
||||
reader = new FileReader(file);
|
||||
while ((len = reader.read(buf)) >= 0) {
|
||||
s.append(buf, 0, len);
|
||||
}
|
||||
return s.toString();
|
||||
}
|
||||
finally {
|
||||
if (reader != null) {
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean writeFile(File file, String content) {
|
||||
try {
|
||||
FileWriter writer = new FileWriter(file, false);
|
||||
writer.write(content);
|
||||
writer.close();
|
||||
return true;
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void log(String error) {
|
||||
System.err.println(error);
|
||||
}
|
||||
}
|
|
@ -396,6 +396,11 @@ class PacketReader {
|
|||
// The server supports sessions
|
||||
connection.getSASLAuthentication().sessionsSupported();
|
||||
}
|
||||
else if (parser.getName().equals("ver")) {
|
||||
if (parser.getNamespace().equals("urn:xmpp:features:rosterver")) {
|
||||
connection.setRosterVersioningSupported();
|
||||
}
|
||||
}
|
||||
else if (parser.getName().equals("compression")) {
|
||||
// The server supports stream compression
|
||||
connection.setAvailableCompressionMethods(PacketParserUtils.parseCompressionMethods(parser));
|
||||
|
|
|
@ -27,9 +27,11 @@ import org.jivesoftware.smack.packet.IQ;
|
|||
import org.jivesoftware.smack.packet.Packet;
|
||||
import org.jivesoftware.smack.packet.Presence;
|
||||
import org.jivesoftware.smack.packet.RosterPacket;
|
||||
import org.jivesoftware.smack.packet.RosterPacket.Item;
|
||||
import org.jivesoftware.smack.util.StringUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
|
@ -55,7 +57,8 @@ public class Roster {
|
|||
*/
|
||||
private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.accept_all;
|
||||
|
||||
private Connection connection;
|
||||
private final Connection connection;
|
||||
private final RosterStore rosterStore;
|
||||
private final Map<String, RosterGroup> groups;
|
||||
private final Map<String,RosterEntry> entries;
|
||||
private final List<RosterEntry> unfiledEntries;
|
||||
|
@ -99,6 +102,7 @@ public class Roster {
|
|||
*/
|
||||
Roster(final Connection connection) {
|
||||
this.connection = connection;
|
||||
rosterStore = connection.getConfiguration().getRosterStore();
|
||||
groups = new ConcurrentHashMap<String, RosterGroup>();
|
||||
unfiledEntries = new CopyOnWriteArrayList<RosterEntry>();
|
||||
entries = new ConcurrentHashMap<String,RosterEntry>();
|
||||
|
@ -188,7 +192,13 @@ public class Roster {
|
|||
throw new IllegalStateException("Anonymous users can't have a roster.");
|
||||
}
|
||||
|
||||
connection.sendPacket(new RosterPacket());
|
||||
RosterPacket packet = new RosterPacket();
|
||||
if (rosterStore != null && connection.isRosterVersioningSupported()) {
|
||||
packet.setVersion(rosterStore.getRosterVersion());
|
||||
PacketFilter filter = new PacketIDFilter(packet.getPacketID());
|
||||
connection.addPacketListener(new RosterResultListener(), filter);
|
||||
}
|
||||
connection.sendPacket(packet);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -660,6 +670,81 @@ public class Roster {
|
|||
}
|
||||
}
|
||||
|
||||
private void addUpdateEntry(Collection<String> addedEntries,
|
||||
Collection<String> updatedEntries, RosterPacket.Item item,
|
||||
RosterEntry entry) {
|
||||
RosterEntry oldEntry = entries.put(item.getUser(), entry);
|
||||
if (oldEntry == null) {
|
||||
addedEntries.add(item.getUser());
|
||||
}
|
||||
else {
|
||||
RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry);
|
||||
if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) {
|
||||
updatedEntries.add(item.getUser());
|
||||
}
|
||||
}
|
||||
|
||||
// Mark the entry as unfiled if it does not belong to any groups.
|
||||
if (item.getGroupNames().isEmpty()) {
|
||||
if (!unfiledEntries.contains(entry)) {
|
||||
unfiledEntries.add(entry);
|
||||
}
|
||||
}
|
||||
else {
|
||||
unfiledEntries.remove(entry);
|
||||
}
|
||||
|
||||
// Add the user to the new groups
|
||||
|
||||
// Add the entry to the groups
|
||||
List<String> newGroupNames = new ArrayList<String>();
|
||||
for (String groupName : item.getGroupNames()) {
|
||||
// Add the group name to the list.
|
||||
newGroupNames.add(groupName);
|
||||
|
||||
// Add the entry to the group.
|
||||
RosterGroup group = getGroup(groupName);
|
||||
if (group == null) {
|
||||
group = createGroup(groupName);
|
||||
groups.put(groupName, group);
|
||||
}
|
||||
// Add the entry.
|
||||
group.addEntryLocal(entry);
|
||||
}
|
||||
|
||||
// Remove user from the remaining groups.
|
||||
List<String> oldGroupNames = new ArrayList<String>();
|
||||
for (RosterGroup group: getGroups()) {
|
||||
oldGroupNames.add(group.getName());
|
||||
}
|
||||
oldGroupNames.removeAll(newGroupNames);
|
||||
|
||||
for (String groupName : oldGroupNames) {
|
||||
RosterGroup group = getGroup(groupName);
|
||||
group.removeEntryLocal(entry);
|
||||
if (group.getEntryCount() == 0) {
|
||||
groups.remove(groupName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteEntry(Collection<String> deletedEntries, RosterEntry entry) {
|
||||
String user = entry.getUser();
|
||||
entries.remove(user);
|
||||
unfiledEntries.remove(entry);
|
||||
presenceMap.remove(StringUtils.parseBareAddress(user));
|
||||
deletedEntries.add(user);
|
||||
|
||||
for (Entry<String,RosterGroup> e: groups.entrySet()) {
|
||||
RosterGroup group = e.getValue();
|
||||
group.removeEntryLocal(entry);
|
||||
if (group.getEntryCount() == 0) {
|
||||
groups.remove(e.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An enumeration for the subscription mode options.
|
||||
*/
|
||||
|
@ -801,123 +886,98 @@ public class Roster {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the case of the empty IQ-result for roster versioning.
|
||||
*
|
||||
* Intended to listen for a concrete roster result and deregisters
|
||||
* itself after a processed packet.
|
||||
*/
|
||||
private class RosterResultListener implements PacketListener {
|
||||
|
||||
@Override
|
||||
public void processPacket(Packet packet) {
|
||||
connection.removePacketListener(this);
|
||||
if (packet instanceof RosterPacket) {
|
||||
// Non-empty roster results are processed by the RosterPacketListener class
|
||||
return;
|
||||
}
|
||||
if (!(packet instanceof IQ)) {
|
||||
return;
|
||||
}
|
||||
IQ result = (IQ)packet;
|
||||
if(result.getType().equals(IQ.Type.RESULT)){
|
||||
Collection<String> addedEntries = new ArrayList<String>();
|
||||
Collection<String> updatedEntries = new ArrayList<String>();
|
||||
for(RosterPacket.Item item : rosterStore.getEntries()){
|
||||
RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
|
||||
item.getItemType(), item.getItemStatus(), Roster.this, connection);
|
||||
addUpdateEntry(addedEntries,updatedEntries,item,entry);
|
||||
}
|
||||
|
||||
synchronized (Roster.this) {
|
||||
rosterInitialized = true;
|
||||
Roster.this.notifyAll();
|
||||
}
|
||||
fireRosterChangedEvent(addedEntries,updatedEntries,
|
||||
Collections.<String>emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for all roster packets and processes them.
|
||||
*/
|
||||
private class RosterPacketListener implements PacketListener {
|
||||
|
||||
public void processPacket(Packet packet) {
|
||||
// Keep a registry of the entries that were added, deleted or updated. An event
|
||||
// will be fired for each affected entry
|
||||
RosterPacket rosterPacket = (RosterPacket) packet;
|
||||
Collection<String> addedEntries = new ArrayList<String>();
|
||||
Collection<String> updatedEntries = new ArrayList<String>();
|
||||
Collection<String> deletedEntries = new ArrayList<String>();
|
||||
|
||||
RosterPacket rosterPacket = (RosterPacket) packet;
|
||||
for (RosterPacket.Item item : rosterPacket.getRosterItems()) {
|
||||
RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
|
||||
item.getItemType(), item.getItemStatus(), Roster.this, connection);
|
||||
String version = rosterPacket.getVersion();
|
||||
|
||||
// If the packet is of the type REMOVE then remove the entry
|
||||
if (RosterPacket.ItemType.remove.equals(item.getItemType())) {
|
||||
// Remove the entry from the entry list.
|
||||
if (entries.containsKey(item.getUser())) {
|
||||
entries.remove(item.getUser());
|
||||
}
|
||||
// Remove the entry from the unfiled entry list.
|
||||
if (unfiledEntries.contains(entry)) {
|
||||
unfiledEntries.remove(entry);
|
||||
}
|
||||
// Removing the user from the roster, so remove any presence information
|
||||
// about them.
|
||||
String key = StringUtils.parseName(item.getUser()) + "@" +
|
||||
StringUtils.parseServer(item.getUser());
|
||||
presenceMap.remove(key);
|
||||
// Keep note that an entry has been removed
|
||||
deletedEntries.add(item.getUser());
|
||||
if (rosterPacket.getType().equals(IQ.Type.SET)) {
|
||||
// Roster push (RFC 6121, 2.1.6)
|
||||
// A roster push with a non-empty from not matching our address MUST be ignored
|
||||
String jid = StringUtils.parseBareAddress(connection.getUser());
|
||||
if (rosterPacket.getFrom() != null &&
|
||||
!rosterPacket.getFrom().equals(jid)) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// Make sure the entry is in the entry list.
|
||||
if (!entries.containsKey(item.getUser())) {
|
||||
entries.put(item.getUser(), entry);
|
||||
// Keep note that an entry has been added
|
||||
addedEntries.add(item.getUser());
|
||||
}
|
||||
else {
|
||||
// If the entry was in then list then update its state with the new values
|
||||
RosterEntry oldEntry = entries.put(item.getUser(), entry);
|
||||
|
||||
RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry);
|
||||
//We have also to check if only the group names have changed from the item
|
||||
if (oldEntry == null || !oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames()))
|
||||
{
|
||||
updatedEntries.add(item.getUser());
|
||||
}
|
||||
}
|
||||
// If the roster entry belongs to any groups, remove it from the
|
||||
// list of unfiled entries.
|
||||
if (!item.getGroupNames().isEmpty()) {
|
||||
unfiledEntries.remove(entry);
|
||||
}
|
||||
// Otherwise add it to the list of unfiled entries.
|
||||
else {
|
||||
if (!unfiledEntries.contains(entry)) {
|
||||
unfiledEntries.add(entry);
|
||||
}
|
||||
// A roster push must contain exactly one entry
|
||||
Collection<Item> items = rosterPacket.getRosterItems();
|
||||
if (items.size() != 1) {
|
||||
return;
|
||||
}
|
||||
Item item = items.iterator().next();
|
||||
processPushItem(addedEntries, updatedEntries, deletedEntries, version, item);
|
||||
|
||||
connection.sendPacket(IQ.createResultIQ(rosterPacket));
|
||||
}
|
||||
else {
|
||||
// Roster result (RFC 6121, 2.1.4)
|
||||
|
||||
// Ignore items without valid subscription type
|
||||
ArrayList<Item> validItems = new ArrayList<RosterPacket.Item>();
|
||||
for (RosterPacket.Item item : rosterPacket.getRosterItems()) {
|
||||
if (hasValidSubscriptionType(item)) {
|
||||
validItems.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the list of groups that the user currently belongs to.
|
||||
List<String> currentGroupNames = new ArrayList<String>();
|
||||
for (RosterGroup group: getGroups()) {
|
||||
if (group.contains(entry)) {
|
||||
currentGroupNames.add(group.getName());
|
||||
}
|
||||
}
|
||||
processResult(addedEntries, updatedEntries, deletedEntries, version, validItems);
|
||||
}
|
||||
|
||||
// If the packet is not of the type REMOVE then add the entry to the groups
|
||||
if (!RosterPacket.ItemType.remove.equals(item.getItemType())) {
|
||||
// Create the new list of groups the user belongs to.
|
||||
List<String> newGroupNames = new ArrayList<String>();
|
||||
for (String groupName : item.getGroupNames()) {
|
||||
// Add the group name to the list.
|
||||
newGroupNames.add(groupName);
|
||||
|
||||
// Add the entry to the group.
|
||||
RosterGroup group = getGroup(groupName);
|
||||
if (group == null) {
|
||||
group = createGroup(groupName);
|
||||
groups.put(groupName, group);
|
||||
}
|
||||
// Add the entry.
|
||||
group.addEntryLocal(entry);
|
||||
}
|
||||
|
||||
// We have the list of old and new group names. We now need to
|
||||
// remove the entry from the all the groups it may no longer belong
|
||||
// to. We do this by subtracting the new group set from the old.
|
||||
for (String newGroupName : newGroupNames) {
|
||||
currentGroupNames.remove(newGroupName);
|
||||
}
|
||||
}
|
||||
|
||||
// Loop through any groups that remain and remove the entries.
|
||||
// This is necessary for the case of remote entry removals.
|
||||
for (String groupName : currentGroupNames) {
|
||||
RosterGroup group = getGroup(groupName);
|
||||
group.removeEntryLocal(entry);
|
||||
if (group.getEntryCount() == 0) {
|
||||
groups.remove(groupName);
|
||||
}
|
||||
}
|
||||
// Remove all the groups with no entries. We have to do this because
|
||||
// RosterGroup.removeEntry removes the entry immediately (locally) and the
|
||||
// group could remain empty.
|
||||
// TODO Check the performance/logic for rosters with large number of groups
|
||||
for (RosterGroup group : getGroups()) {
|
||||
if (group.getEntryCount() == 0) {
|
||||
groups.remove(group.getName());
|
||||
}
|
||||
// Remove all the groups with no entries. We have to do this because
|
||||
// RosterGroup.removeEntry removes the entry immediately (locally) and the
|
||||
// group could remain empty.
|
||||
// TODO Check the performance/logic for rosters with large number of groups
|
||||
for (RosterGroup group : getGroups()) {
|
||||
if (group.getEntryCount() == 0) {
|
||||
groups.remove(group.getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -930,5 +990,58 @@ public class Roster {
|
|||
// Fire event for roster listeners.
|
||||
fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
|
||||
}
|
||||
|
||||
private void processPushItem(Collection<String> addedEntries, Collection<String> updatedEntries,
|
||||
Collection<String> deletedEntries, String version, Item item) {
|
||||
RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
|
||||
item.getItemType(), item.getItemStatus(), Roster.this, connection);
|
||||
|
||||
if (item.getItemType().equals(RosterPacket.ItemType.remove)) {
|
||||
deleteEntry(deletedEntries, entry);
|
||||
if (rosterStore != null) {
|
||||
rosterStore.removeEntry(entry.getUser(), version);
|
||||
}
|
||||
}
|
||||
else if (hasValidSubscriptionType(item)) {
|
||||
addUpdateEntry(addedEntries, updatedEntries, item, entry);
|
||||
if (rosterStore != null) {
|
||||
rosterStore.addEntry(item, version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processResult(Collection<String> addedEntries, Collection<String> updatedEntries,
|
||||
Collection<String> deletedEntries, String version, Collection<Item> items) {
|
||||
for (RosterPacket.Item item : items) {
|
||||
RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
|
||||
item.getItemType(), item.getItemStatus(), Roster.this, connection);
|
||||
addUpdateEntry(addedEntries, updatedEntries, item, entry);
|
||||
}
|
||||
List<String> toDelete = new ArrayList<String>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
|
|
|
@ -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<RosterEntry> entries;
|
||||
private final Set<RosterEntry> 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<RosterEntry>();
|
||||
entries = new LinkedHashSet<RosterEntry>();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
|
67
source/org/jivesoftware/smack/RosterStore.java
Normal file
67
source/org/jivesoftware/smack/RosterStore.java
Normal file
|
@ -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<RosterPacket.Item> 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<RosterPacket.Item> 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);
|
||||
|
||||
}
|
|
@ -33,6 +33,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
|
|||
public class RosterPacket extends IQ {
|
||||
|
||||
private final List<Item> rosterItems = new ArrayList<Item>();
|
||||
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("<query xmlns=\"jabber:iq:roster\">");
|
||||
buf.append("<query xmlns=\"jabber:iq:roster\"");
|
||||
if (rosterVersion != null) {
|
||||
buf.append(" ver=\"");
|
||||
buf.append(rosterVersion);
|
||||
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("<item jid=\"").append(user).append("\"");
|
||||
buf.append("<item jid=\"").append(StringUtils.escapeForXML(user)).append("\"");
|
||||
if (name != null) {
|
||||
buf.append(" name=\"").append(StringUtils.escapeForXML(name)).append("\"");
|
||||
}
|
||||
|
@ -215,54 +230,84 @@ public class RosterPacket extends IQ {
|
|||
buf.append("</item>");
|
||||
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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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("&#x");
|
||||
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.
|
||||
*
|
||||
* <strong>Warning:</strong> 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.
|
||||
*/
|
||||
|
|
222
test-unit/org/jivesoftware/smack/DefaultRosterStoreTest.java
Normal file
222
test-unit/org/jivesoftware/smack/DefaultRosterStoreTest.java
Normal file
|
@ -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<Item> 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<Item> items34 = new ArrayList<RosterPacket.Item>();
|
||||
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());
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <a href="http://xmpp.org/rfcs/rfc6121.html#roster-syntax-actions-push">RFC 6121, Section 2.1.6</a>
|
||||
*/
|
||||
@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.
|
||||
|
|
242
test-unit/org/jivesoftware/smack/RosterVersioningTest.java
Normal file
242
test-unit/org/jivesoftware/smack/RosterVersioningTest.java
Normal file
|
@ -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 <a href="http://xmpp.org/rfcs/rfc6121.html#roster">Managing the Roster</a>
|
||||
* @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<RosterEntry> entries = roster.getEntries();
|
||||
assertSame("Size of the roster", 3, entries.size());
|
||||
|
||||
HashSet<Item> items = new HashSet<Item>();
|
||||
for (RosterEntry entry : entries) {
|
||||
items.add(RosterEntry.toRosterItem(entry));
|
||||
}
|
||||
RosterStore store = DefaultRosterStore.init(tmpFolder.newFolder());
|
||||
populateStore(store);
|
||||
assertEquals("Elements of the roster", new HashSet<Item>(store.getEntries()), items);
|
||||
|
||||
for (RosterEntry entry : entries) {
|
||||
assertTrue("joe stevens".equals(entry.getName()) || "geoff hurley".equals(entry.getName())
|
||||
|| "higgins mcmann".equals(entry.getName()));
|
||||
}
|
||||
Collection<RosterGroup> 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<Item> rosterItems = new HashSet<Item>();
|
||||
for (RosterEntry entry : roster.getEntries()) {
|
||||
rosterItems.add(RosterEntry.toRosterItem(entry));
|
||||
}
|
||||
assertEquals(rosterItems, new HashSet<Item>(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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue