1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2025-09-09 17:19:39 +02:00

Create smack-im subproject for XMPP-IM

Move Roster and Chat(Manager) code into their own packages within the
new smack-im subproject.

Apply Manager pattern to Roster.

Fixes SMACK-637.
This commit is contained in:
Florian Schmaus 2015-01-22 13:53:50 +01:00
parent e722018808
commit d5b8647d9d
47 changed files with 392 additions and 271 deletions

View file

@ -0,0 +1,203 @@
/**
*
* Copyright 2003-2007 Jive Software.
*
* 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.chat;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.util.StringUtils;
import java.util.Set;
import java.util.Collections;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* A chat is a series of messages sent between two users. Each chat has a unique
* thread ID, which is used to track which messages are part of a particular
* conversation. Some messages are sent without a thread ID, and some clients
* don't send thread IDs at all. Therefore, if a message without a thread ID
* arrives it is routed to the most recently created Chat with the message
* sender.
*
* @author Matt Tucker
*/
public class Chat {
private ChatManager chatManager;
private String threadID;
private String participant;
private final Set<ChatMessageListener> listeners = new CopyOnWriteArraySet<ChatMessageListener>();
/**
* Creates a new chat with the specified user and thread ID.
*
* @param chatManager the chatManager the chat will use.
* @param participant the user to chat with.
* @param threadID the thread ID to use.
*/
Chat(ChatManager chatManager, String participant, String threadID) {
if (StringUtils.isEmpty(threadID)) {
throw new IllegalArgumentException("Thread ID must not be null");
}
this.chatManager = chatManager;
this.participant = participant;
this.threadID = threadID;
}
/**
* Returns the thread id associated with this chat, which corresponds to the
* <tt>thread</tt> field of XMPP messages. This method may return <tt>null</tt>
* if there is no thread ID is associated with this Chat.
*
* @return the thread ID of this chat.
*/
public String getThreadID() {
return threadID;
}
/**
* Returns the name of the user the chat is with.
*
* @return the name of the user the chat is occuring with.
*/
public String getParticipant() {
return participant;
}
/**
* Sends the specified text as a message to the other chat participant.
* This is a convenience method for:
*
* <pre>
* Message message = chat.createMessage();
* message.setBody(messageText);
* chat.sendMessage(message);
* </pre>
*
* @param text the text to send.
* @throws NotConnectedException
*/
public void sendMessage(String text) throws NotConnectedException {
Message message = new Message();
message.setBody(text);
sendMessage(message);
}
/**
* Sends a message to the other chat participant. The thread ID, recipient,
* and message type of the message will automatically set to those of this chat.
*
* @param message the message to send.
* @throws NotConnectedException
*/
public void sendMessage(Message message) throws NotConnectedException {
// Force the recipient, message type, and thread ID since the user elected
// to send the message through this chat object.
message.setTo(participant);
message.setType(Message.Type.chat);
message.setThread(threadID);
chatManager.sendMessage(this, message);
}
/**
* Adds a packet listener that will be notified of any new messages in the
* chat.
*
* @param listener a packet listener.
*/
public void addMessageListener(ChatMessageListener listener) {
if(listener == null) {
return;
}
// TODO these references should be weak.
listeners.add(listener);
}
public void removeMessageListener(ChatMessageListener listener) {
listeners.remove(listener);
}
/**
* Closes the Chat and removes all references to it from the {@link ChatManager}. The chat will
* be unusable when this method returns, so it's recommend to drop all references to the
* instance right after calling {@link #close()}.
*/
public void close() {
chatManager.closeChat(this);
listeners.clear();
}
/**
* Returns an unmodifiable set of all of the listeners registered with this chat.
*
* @return an unmodifiable set of all of the listeners registered with this chat.
*/
public Set<ChatMessageListener> getListeners() {
return Collections.unmodifiableSet(listeners);
}
/**
* Creates a {@link org.jivesoftware.smack.PacketCollector} which will accumulate the Messages
* for this chat. Always cancel PacketCollectors when finished with them as they will accumulate
* messages indefinitely.
*
* @return the PacketCollector which returns Messages for this chat.
*/
public PacketCollector createCollector() {
return chatManager.createPacketCollector(this);
}
/**
* Delivers a message directly to this chat, which will add the message
* to the collector and deliver it to all listeners registered with the
* Chat. This is used by the XMPPConnection class to deliver messages
* without a thread ID.
*
* @param message the message.
*/
void deliver(Message message) {
// Because the collector and listeners are expecting a thread ID with
// a specific value, set the thread ID on the message even though it
// probably never had one.
message.setThread(threadID);
for (ChatMessageListener listener : listeners) {
listener.processMessage(this, message);
}
}
@Override
public String toString() {
return "Chat [(participant=" + participant + "), (thread=" + threadID + ")]";
}
@Override
public int hashCode() {
int hash = 1;
hash = hash * 31 + threadID.hashCode();
hash = hash * 31 + participant.hashCode();
return hash;
}
@Override
public boolean equals(Object obj) {
return obj instanceof Chat
&& threadID.equals(((Chat)obj).getThreadID())
&& participant.equals(((Chat)obj).getParticipant());
}
}

View file

@ -0,0 +1,405 @@
/**
*
* Copyright 2003-2007 Jive Software.
*
* 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.chat;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import org.jivesoftware.smack.Manager;
import org.jivesoftware.smack.MessageListener;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.FlexiblePacketTypeFilter;
import org.jivesoftware.smack.filter.FromMatchesFilter;
import org.jivesoftware.smack.filter.MessageTypeFilter;
import org.jivesoftware.smack.filter.OrFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.ThreadFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Message.Type;
import org.jivesoftware.smack.packet.Packet;
import org.jxmpp.util.XmppStringUtils;
/**
* The chat manager keeps track of references to all current chats. It will not hold any references
* in memory on its own so it is necessary to keep a reference to the chat object itself. To be
* made aware of new chats, register a listener by calling {@link #addChatListener(ChatManagerListener)}.
*
* @author Alexander Wenckus
*/
public class ChatManager extends Manager{
private static final Map<XMPPConnection, ChatManager> INSTANCES = new WeakHashMap<XMPPConnection, ChatManager>();
/**
* Sets the default behaviour for allowing 'normal' messages to be used in chats. As some clients don't set
* the message type to chat, the type normal has to be accepted to allow chats with these clients.
*/
private static boolean defaultIsNormalInclude = true;
/**
* Sets the default behaviour for how to match chats when there is NO thread id in the incoming message.
*/
private static MatchMode defaultMatchMode = MatchMode.BARE_JID;
/**
* Returns the ChatManager instance associated with a given XMPPConnection.
*
* @param connection the connection used to look for the proper ServiceDiscoveryManager.
* @return the ChatManager associated with a given XMPPConnection.
*/
public static synchronized ChatManager getInstanceFor(XMPPConnection connection) {
ChatManager manager = INSTANCES.get(connection);
if (manager == null)
manager = new ChatManager(connection);
return manager;
}
/**
* Defines the different modes under which a match will be attempted with an existing chat when
* the incoming message does not have a thread id.
*/
public enum MatchMode {
/**
* Will not attempt to match, always creates a new chat.
*/
NONE,
/**
* Will match on the JID in the from field of the message.
*/
SUPPLIED_JID,
/**
* Will attempt to match on the JID in the from field, and then attempt the base JID if no match was found.
* This is the most lenient matching.
*/
BARE_JID;
}
private final PacketFilter packetFilter = new OrFilter(MessageTypeFilter.CHAT, new FlexiblePacketTypeFilter<Message>() {
@Override
protected boolean acceptSpecific(Message message) {
return normalIncluded ? message.getType() == Type.normal : false;
}
});
/**
* Determines whether incoming messages of type normal can create chats.
*/
private boolean normalIncluded = defaultIsNormalInclude;
/**
* Determines how incoming message with no thread will be matched to existing chats.
*/
private MatchMode matchMode = defaultMatchMode;
/**
* Maps thread ID to chat.
*/
private Map<String, Chat> threadChats = new ConcurrentHashMap<>();
/**
* Maps jids to chats
*/
private Map<String, Chat> jidChats = new ConcurrentHashMap<>();
/**
* Maps base jids to chats
*/
private Map<String, Chat> baseJidChats = new ConcurrentHashMap<>();
private Set<ChatManagerListener> chatManagerListeners
= new CopyOnWriteArraySet<ChatManagerListener>();
private Map<MessageListener, PacketFilter> interceptors
= new WeakHashMap<MessageListener, PacketFilter>();
private ChatManager(XMPPConnection connection) {
super(connection);
// Add a listener for all message packets so that we can deliver
// messages to the best Chat instance available.
connection.addSyncPacketListener(new PacketListener() {
public void processPacket(Packet packet) {
Message message = (Message) packet;
Chat chat;
if (message.getThread() == null) {
chat = getUserChat(message.getFrom());
}
else {
chat = getThreadChat(message.getThread());
}
if(chat == null) {
chat = createChat(message);
}
// The chat could not be created, abort here
if (chat == null)
return;
deliverMessage(chat, message);
}
}, packetFilter);
INSTANCES.put(connection, this);
}
/**
* Determines whether incoming messages of type <i>normal</i> will be used for creating new chats or matching
* a message to existing ones.
*
* @return true if normal is allowed, false otherwise.
*/
public boolean isNormalIncluded() {
return normalIncluded;
}
/**
* Sets whether to allow incoming messages of type <i>normal</i> to be used for creating new chats or matching
* a message to an existing one.
*
* @param normalIncluded true to allow normal, false otherwise.
*/
public void setNormalIncluded(boolean normalIncluded) {
this.normalIncluded = normalIncluded;
}
/**
* Gets the current mode for matching messages with <b>NO</b> thread id to existing chats.
*
* @return The current mode.
*/
public MatchMode getMatchMode() {
return matchMode;
}
/**
* Sets the mode for matching messages with <b>NO</b> thread id to existing chats.
*
* @param matchMode The mode to set.
*/
public void setMatchMode(MatchMode matchMode) {
this.matchMode = matchMode;
}
/**
* Creates a new chat and returns it.
*
* @param userJID the user this chat is with.
* @return the created chat.
*/
public Chat createChat(String userJID) {
return createChat(userJID, null);
}
/**
* Creates a new chat and returns it.
*
* @param userJID the user this chat is with.
* @param listener the optional listener which will listen for new messages from this chat.
* @return the created chat.
*/
public Chat createChat(String userJID, ChatMessageListener listener) {
return createChat(userJID, null, listener);
}
/**
* Creates a new chat using the specified thread ID, then returns it.
*
* @param userJID the jid of the user this chat is with
* @param thread the thread of the created chat.
* @param listener the optional listener to add to the chat
* @return the created chat.
*/
public Chat createChat(String userJID, String thread, ChatMessageListener listener) {
if (thread == null) {
thread = nextID();
}
Chat chat = threadChats.get(thread);
if(chat != null) {
throw new IllegalArgumentException("ThreadID is already used");
}
chat = createChat(userJID, thread, true);
chat.addMessageListener(listener);
return chat;
}
private Chat createChat(String userJID, String threadID, boolean createdLocally) {
Chat chat = new Chat(this, userJID, threadID);
threadChats.put(threadID, chat);
jidChats.put(userJID, chat);
baseJidChats.put(XmppStringUtils.parseBareJid(userJID), chat);
for(ChatManagerListener listener : chatManagerListeners) {
listener.chatCreated(chat, createdLocally);
}
return chat;
}
void closeChat(Chat chat) {
threadChats.remove(chat.getThreadID());
String userJID = chat.getParticipant();
jidChats.remove(userJID);
baseJidChats.remove(XmppStringUtils.parseBareJid(userJID));
}
/**
* Creates a new {@link Chat} based on the message. May returns null if no chat could be
* created, e.g. because the message comes without from.
*
* @param message
* @return a Chat or null if none can be created
*/
private Chat createChat(Message message) {
String userJID = message.getFrom();
// According to RFC6120 8.1.2.1 4. messages without a 'from' attribute are valid, but they
// are of no use in this case for ChatManager
if (userJID == null) {
return null;
}
String threadID = message.getThread();
if(threadID == null) {
threadID = nextID();
}
return createChat(userJID, threadID, false);
}
/**
* Try to get a matching chat for the given user JID, based on the {@link MatchMode}.
* <li>NONE - return null
* <li>SUPPLIED_JID - match the jid in the from field of the message exactly.
* <li>BARE_JID - if not match for from field, try the bare jid.
*
* @param userJID jid in the from field of message.
* @return Matching chat, or null if no match found.
*/
private Chat getUserChat(String userJID) {
if (matchMode == MatchMode.NONE) {
return null;
}
// According to RFC6120 8.1.2.1 4. messages without a 'from' attribute are valid, but they
// are of no use in this case for ChatManager
if (userJID == null) {
return null;
}
Chat match = jidChats.get(userJID);
if (match == null && (matchMode == MatchMode.BARE_JID)) {
match = baseJidChats.get(XmppStringUtils.parseBareJid(userJID));
}
return match;
}
public Chat getThreadChat(String thread) {
return threadChats.get(thread);
}
/**
* Register a new listener with the ChatManager to recieve events related to chats.
*
* @param listener the listener.
*/
public void addChatListener(ChatManagerListener listener) {
chatManagerListeners.add(listener);
}
/**
* Removes a listener, it will no longer be notified of new events related to chats.
*
* @param listener the listener that is being removed
*/
public void removeChatListener(ChatManagerListener listener) {
chatManagerListeners.remove(listener);
}
/**
* Returns an unmodifiable set of all chat listeners currently registered with this
* manager.
*
* @return an unmodifiable collection of all chat listeners currently registered with this
* manager.
*/
public Set<ChatManagerListener> getChatListeners() {
return Collections.unmodifiableSet(chatManagerListeners);
}
private void deliverMessage(Chat chat, Message message) {
// Here we will run any interceptors
chat.deliver(message);
}
void sendMessage(Chat chat, Message message) throws NotConnectedException {
for(Map.Entry<MessageListener, PacketFilter> interceptor : interceptors.entrySet()) {
PacketFilter filter = interceptor.getValue();
if(filter != null && filter.accept(message)) {
interceptor.getKey().processMessage(message);
}
}
// Ensure that messages being sent have a proper FROM value
if (message.getFrom() == null) {
message.setFrom(connection().getUser());
}
connection().sendPacket(message);
}
PacketCollector createPacketCollector(Chat chat) {
return connection().createPacketCollector(new AndFilter(new ThreadFilter(chat.getThreadID()),
FromMatchesFilter.create(chat.getParticipant())));
}
/**
* Adds an interceptor which intercepts any messages sent through chats.
*
* @param messageInterceptor the interceptor.
*/
public void addOutgoingMessageInterceptor(MessageListener messageInterceptor) {
addOutgoingMessageInterceptor(messageInterceptor, null);
}
public void addOutgoingMessageInterceptor(MessageListener messageInterceptor, PacketFilter filter) {
if (messageInterceptor == null) {
return;
}
interceptors.put(messageInterceptor, filter);
}
/**
* Returns a unique id.
*
* @return the next id.
*/
private static String nextID() {
return UUID.randomUUID().toString();
}
public static void setDefaultMatchMode(MatchMode mode) {
defaultMatchMode = mode;
}
public static void setDefaultIsNormalIncluded(boolean allowNormal) {
defaultIsNormalInclude = allowNormal;
}
}

View file

@ -0,0 +1,34 @@
/**
*
* Copyright 2003-2007 Jive Software.
*
* 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.chat;
/**
* A listener for chat related events.
*
* @author Alexander Wenckus
*/
public interface ChatManagerListener {
/**
* Event fired when a new chat is created.
*
* @param chat the chat that was created.
* @param createdLocally true if the chat was created by the local user and false if it wasn't.
*/
void chatCreated(Chat chat, boolean createdLocally);
}

View file

@ -0,0 +1,27 @@
/**
*
* Copyright 2003-2007 Jive Software.
*
* 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.chat;
import org.jivesoftware.smack.packet.Message;
/**
*
*/
public interface ChatMessageListener {
void processMessage(Chat chat, Message message);
}

View file

@ -0,0 +1,33 @@
/**
*
* Copyright © 2015 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.im;
import org.jivesoftware.smack.initializer.UrlInitializer;
public class SmackImInitializer extends UrlInitializer {
@Override
protected String getProvidersUrl() {
return "classpath:org.jivesoftware.smack.im/smackim.providers";
}
@Override
protected String getConfigUrl() {
return "classpath:org.jivesoftware.smack.im/smackim.xml";
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,248 @@
/**
*
* Copyright 2003-2007 Jive Software.
*
* 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.roster;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.roster.packet.RosterPacket;
/**
* Each user in your roster is represented by a roster entry, which contains the user's
* JID and a name or nickname you assign.
*
* @author Matt Tucker
*/
public class RosterEntry {
private String user;
private String name;
private RosterPacket.ItemType type;
private RosterPacket.ItemStatus status;
final private Roster roster;
final private XMPPConnection connection;
/**
* Creates a new roster entry.
*
* @param user the user.
* @param name the nickname for the entry.
* @param type the subscription type.
* @param status the subscription status (related to subscriptions pending to be approbed).
* @param connection a connection to the XMPP server.
*/
RosterEntry(String user, String name, RosterPacket.ItemType type,
RosterPacket.ItemStatus status, Roster roster, XMPPConnection connection) {
this.user = user;
this.name = name;
this.type = type;
this.status = status;
this.roster = roster;
this.connection = connection;
}
/**
* Returns the JID of the user associated with this entry.
*
* @return the user associated with this entry.
*/
public String getUser() {
return user;
}
/**
* Returns the name associated with this entry.
*
* @return the name.
*/
public String getName() {
return name;
}
/**
* Sets the name associated with this entry.
*
* @param name the name.
* @throws NotConnectedException
*/
public void setName(String name) throws NotConnectedException {
// Do nothing if the name hasn't changed.
if (name != null && name.equals(this.name)) {
return;
}
this.name = name;
RosterPacket packet = new RosterPacket();
packet.setType(IQ.Type.set);
packet.addRosterItem(toRosterItem(this));
connection.sendPacket(packet);
}
/**
* Updates the state of the entry with the new values.
*
* @param name the nickname for the entry.
* @param type the subscription type.
* @param status the subscription status (related to subscriptions pending to be approbed).
*/
void updateState(String name, RosterPacket.ItemType type, RosterPacket.ItemStatus status) {
this.name = name;
this.type = type;
this.status = status;
}
/**
* Returns an copied list of the roster groups that this entry belongs to.
*
* @return an iterator for the groups this entry belongs to.
*/
public List<RosterGroup> getGroups() {
List<RosterGroup> results = new ArrayList<RosterGroup>();
// Loop through all roster groups and find the ones that contain this
// entry. This algorithm should be fine
for (RosterGroup group: roster.getGroups()) {
if (group.contains(this)) {
results.add(group);
}
}
return results;
}
/**
* Returns the roster subscription type of the entry. When the type is
* RosterPacket.ItemType.none or RosterPacket.ItemType.from,
* refer to {@link RosterEntry getStatus()} to see if a subscription request
* is pending.
*
* @return the type.
*/
public RosterPacket.ItemType getType() {
return type;
}
/**
* Returns the roster subscription status of the entry. When the status is
* RosterPacket.ItemStatus.SUBSCRIPTION_PENDING, the contact has to answer the
* subscription request.
*
* @return the status.
*/
public RosterPacket.ItemStatus getStatus() {
return status;
}
public String toString() {
StringBuilder buf = new StringBuilder();
if (name != null) {
buf.append(name).append(": ");
}
buf.append(user);
Collection<RosterGroup> groups = getGroups();
if (!groups.isEmpty()) {
buf.append(" [");
Iterator<RosterGroup> iter = groups.iterator();
RosterGroup group = iter.next();
buf.append(group.getName());
while (iter.hasNext()) {
buf.append(", ");
group = iter.next();
buf.append(group.getName());
}
buf.append("]");
}
return buf.toString();
}
@Override
public int hashCode() {
return (user == null ? 0 : user.hashCode());
}
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object != null && object instanceof RosterEntry) {
return user.equals(((RosterEntry)object).getUser());
}
else {
return false;
}
}
/**
* Indicates whether some other object is "equal to" this by comparing all members.
* <p>
* The {@link #equals(Object)} method returns <code>true</code> if the user JIDs are equal.
*
* @param obj the reference object with which to compare.
* @return <code>true</code> if this object is the same as the obj argument; <code>false</code>
* otherwise.
*/
public boolean equalsDeep(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
RosterEntry other = (RosterEntry) obj;
if (name == null) {
if (other.name != null)
return false;
}
else if (!name.equals(other.name))
return false;
if (status == null) {
if (other.status != null)
return false;
}
else if (!status.equals(other.status))
return false;
if (type == null) {
if (other.type != null)
return false;
}
else if (!type.equals(other.type))
return false;
if (user == null) {
if (other.user != null)
return false;
}
else if (!user.equals(other.user))
return false;
return true;
}
static RosterPacket.Item toRosterItem(RosterEntry entry) {
RosterPacket.Item item = new RosterPacket.Item(entry.getUser(), entry.getName());
item.setItemType(entry.getType());
item.setItemStatus(entry.getStatus());
// Set the correct group names for the item.
for (RosterGroup group : entry.getGroups()) {
item.addGroupName(group.getName());
}
return item;
}
}

View file

@ -0,0 +1,240 @@
/**
*
* Copyright 2003-2007 Jive Software.
*
* 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.roster;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jxmpp.util.XmppStringUtils;
/**
* A group of roster entries.
*
* @see Roster#getGroup(String)
* @author Matt Tucker
*/
public class RosterGroup {
private String name;
private XMPPConnection connection;
private final Set<RosterEntry> entries;
/**
* Creates a new roster group instance.
*
* @param name the name of the group.
* @param connection the connection the group belongs to.
*/
RosterGroup(String name, XMPPConnection connection) {
this.name = name;
this.connection = connection;
entries = new LinkedHashSet<RosterEntry>();
}
/**
* Returns the name of the group.
*
* @return the name of the group.
*/
public String getName() {
return name;
}
/**
* Sets the name of the group. Changing the group's name is like moving all the group entries
* of the group to a new group specified by the new name. Since this group won't have entries
* it will be removed from the roster. This means that all the references to this object will
* be invalid and will need to be updated to the new group specified by the new name.
*
* @param name the name of the group.
* @throws NotConnectedException
*/
public void setName(String name) throws NotConnectedException {
synchronized (entries) {
for (RosterEntry entry : entries) {
RosterPacket packet = new RosterPacket();
packet.setType(IQ.Type.set);
RosterPacket.Item item = RosterEntry.toRosterItem(entry);
item.removeGroupName(this.name);
item.addGroupName(name);
packet.addRosterItem(item);
connection.sendPacket(packet);
}
}
}
/**
* Returns the number of entries in the group.
*
* @return the number of entries in the group.
*/
public int getEntryCount() {
synchronized (entries) {
return entries.size();
}
}
/**
* Returns an copied list of all entries in the group.
*
* @return all entries in the group.
*/
public List<RosterEntry> getEntries() {
synchronized (entries) {
return new ArrayList<RosterEntry>(entries);
}
}
/**
* Returns the roster entry associated with the given XMPP address or
* <tt>null</tt> if the user is not an entry in the group.
*
* @param user the XMPP address of the user (eg "jsmith@example.com").
* @return the roster entry or <tt>null</tt> if it does not exist in the group.
*/
public RosterEntry getEntry(String user) {
if (user == null) {
return null;
}
// Roster entries never include a resource so remove the resource
// if it's a part of the XMPP address.
user = XmppStringUtils.parseBareJid(user);
String userLowerCase = user.toLowerCase(Locale.US);
synchronized (entries) {
for (RosterEntry entry : entries) {
if (entry.getUser().equals(userLowerCase)) {
return entry;
}
}
}
return null;
}
/**
* Returns true if the specified entry is part of this group.
*
* @param entry a roster entry.
* @return true if the entry is part of this group.
*/
public boolean contains(RosterEntry entry) {
synchronized (entries) {
return entries.contains(entry);
}
}
/**
* Returns true if the specified XMPP address is an entry in this group.
*
* @param user the XMPP address of the user.
* @return true if the XMPP address is an entry in this group.
*/
public boolean contains(String user) {
return getEntry(user) != null;
}
/**
* Adds a roster entry to this group. If the entry was unfiled then it will be removed from
* the unfiled list and will be added to this group.
* Note that this is a synchronous call -- Smack must wait for the server
* to receive the updated roster.
*
* @param entry a roster entry.
* @throws XMPPErrorException if an error occured while trying to add the entry to the group.
* @throws NoResponseException if there was no response from the server.
* @throws NotConnectedException
*/
public void addEntry(RosterEntry entry) throws NoResponseException, XMPPErrorException, NotConnectedException {
PacketCollector collector = null;
// Only add the entry if it isn't already in the list.
synchronized (entries) {
if (!entries.contains(entry)) {
RosterPacket packet = new RosterPacket();
packet.setType(IQ.Type.set);
RosterPacket.Item item = RosterEntry.toRosterItem(entry);
item.addGroupName(getName());
packet.addRosterItem(item);
// Wait up to a certain number of seconds for a reply from the server.
collector = connection.createPacketCollectorAndSend(packet);
}
}
if (collector != null) {
collector.nextResultOrThrow();
}
}
/**
* Removes a roster entry from this group. If the entry does not belong to any other group
* then it will be considered as unfiled, therefore it will be added to the list of unfiled
* entries.
* Note that this is a synchronous call -- Smack must wait for the server
* to receive the updated roster.
*
* @param entry a roster entry.
* @throws XMPPErrorException if an error occurred while trying to remove the entry from the group.
* @throws NoResponseException if there was no response from the server.
* @throws NotConnectedException
*/
public void removeEntry(RosterEntry entry) throws NoResponseException, XMPPErrorException, NotConnectedException {
PacketCollector collector = null;
// Only remove the entry if it's in the entry list.
// Remove the entry locally, if we wait for RosterPacketListenerprocess>>Packet(Packet)
// to take place the entry will exist in the group until a packet is received from the
// server.
synchronized (entries) {
if (entries.contains(entry)) {
RosterPacket packet = new RosterPacket();
packet.setType(IQ.Type.set);
RosterPacket.Item item = RosterEntry.toRosterItem(entry);
item.removeGroupName(this.getName());
packet.addRosterItem(item);
// Wait up to a certain number of seconds for a reply from the server.
collector = connection.createPacketCollectorAndSend(packet);
}
}
if (collector != null) {
collector.nextResultOrThrow();
}
}
void addEntryLocal(RosterEntry entry) {
// Update the entry if it is already in the list
synchronized (entries) {
entries.remove(entry);
entries.add(entry);
}
}
void removeEntryLocal(RosterEntry entry) {
// Only remove the entry if it's in the entry list.
synchronized (entries) {
if (entries.contains(entry)) {
entries.remove(entry);
}
}
}
}

View file

@ -0,0 +1,80 @@
/**
*
* Copyright 2003-2007 Jive Software.
*
* 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.roster;
import org.jivesoftware.smack.packet.Presence;
import java.util.Collection;
/**
* A listener that is fired any time a roster is changed or the presence of
* a user in the roster is changed.
*
* @see Roster#addRosterListener(RosterListener)
* @author Matt Tucker
*/
public interface RosterListener {
/**
* Called when roster entries are added.
*
* @param addresses the XMPP addresses of the contacts that have been added to the roster.
*/
public void entriesAdded(Collection<String> addresses);
/**
* Called when a roster entries are updated.
*
* @param addresses the XMPP addresses of the contacts whose entries have been updated.
*/
public void entriesUpdated(Collection<String> addresses);
/**
* Called when a roster entries are removed.
*
* @param addresses the XMPP addresses of the contacts that have been removed from the roster.
*/
public void entriesDeleted(Collection<String> addresses);
/**
* Called when the presence of a roster entry is changed. Care should be taken
* when using the presence data delivered as part of this event. Specifically,
* when a user account is online with multiple resources, the UI should account
* for that. For example, say a user is online with their desktop computer and
* mobile phone. If the user logs out of the IM client on their mobile phone, the
* user should not be shown in the roster (contact list) as offline since they're
* still available as another resource.<p>
*
* To get the current "best presence" for a user after the presence update, query the roster:
* <pre>
* String user = presence.getFrom();
* Presence bestPresence = roster.getPresence(user);
* </pre>
*
* That will return the presence value for the user with the highest priority and
* availability.
*
* Note that this listener is triggered for presence (mode) changes only
* (e.g presence of types available and unavailable. Subscription-related
* presence packets will not cause this method to be called.
*
* @param presence the presence that changed.
* @see Roster#getPresence(String)
*/
public void presenceChanged(Presence presence);
}

View file

@ -0,0 +1,347 @@
/**
*
* Copyright 2003-2007 Jive Software.
*
* 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.roster.packet;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.util.XmlStringBuilder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* Represents XMPP roster packets.
*
* @author Matt Tucker
*/
public class RosterPacket extends IQ {
public static final String ELEMENT = QUERY_ELEMENT;
public static final String NAMESPACE = "jabber:iq:roster";
private final List<Item> rosterItems = new ArrayList<Item>();
private String rosterVersion;
public RosterPacket() {
super(ELEMENT, NAMESPACE);
}
/**
* Adds a roster item to the packet.
*
* @param item a roster item.
*/
public void addRosterItem(Item item) {
synchronized (rosterItems) {
rosterItems.add(item);
}
}
/**
* Returns the number of roster items in this roster packet.
*
* @return the number of roster items.
*/
public int getRosterItemCount() {
synchronized (rosterItems) {
return rosterItems.size();
}
}
/**
* Returns a copied list of the roster items in the packet.
*
* @return a copied list of the roster items in the packet.
*/
public List<Item> getRosterItems() {
synchronized (rosterItems) {
return new ArrayList<Item>(rosterItems);
}
}
@Override
protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder buf) {
buf.optAttribute("ver", rosterVersion);
buf.rightAngleBracket();
synchronized (rosterItems) {
for (Item entry : rosterItems) {
buf.append(entry.toXML());
}
}
return buf;
}
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.
*/
public static class Item {
public static final String GROUP = "group";
private String user;
private String name;
private ItemType itemType;
private ItemStatus itemStatus;
private final Set<String> 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.toLowerCase(Locale.US);
this.name = name;
itemType = null;
itemStatus = null;
groupNames = new CopyOnWriteArraySet<String>();
}
/**
* 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 the roster item type.
*
* @return the roster item type.
*/
public ItemType getItemType() {
return itemType;
}
/**
* Sets the roster item type.
*
* @param itemType the roster item type.
*/
public void setItemType(ItemType itemType) {
this.itemType = itemType;
}
/**
* Returns the roster item status.
*
* @return the roster item status.
*/
public ItemStatus getItemStatus() {
return itemStatus;
}
/**
* Sets the roster item status.
*
* @param itemStatus the roster item status.
*/
public void setItemStatus(ItemStatus itemStatus) {
this.itemStatus = itemStatus;
}
/**
* Returns an unmodifiable set of the group names that the roster item
* belongs to.
*
* @return an unmodifiable set of the group names.
*/
public Set<String> getGroupNames() {
return Collections.unmodifiableSet(groupNames);
}
/**
* Adds a group name.
*
* @param groupName the group name.
*/
public void addGroupName(String groupName) {
groupNames.add(groupName);
}
/**
* Removes a group name.
*
* @param groupName the group name.
*/
public void removeGroupName(String groupName) {
groupNames.remove(groupName);
}
public XmlStringBuilder toXML() {
XmlStringBuilder xml = new XmlStringBuilder();
xml.halfOpenElement(Packet.ITEM).attribute("jid", user);
xml.optAttribute("name", name);
xml.optAttribute("subscription", itemType);
xml.optAttribute("ask", itemStatus);
xml.rightAngleBracket();
for (String groupName : groupNames) {
xml.openElement(GROUP).escape(groupName).closeElement(GROUP);
}
xml.closeElement(Packet.ITEM);
return xml;
}
@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 enum ItemStatus {
/**
* Request to subscribe
*/
subscribe,
/**
* Request to unsubscribe
*/
unsubscribe;
public static final ItemStatus SUBSCRIPTION_PENDING = subscribe;
public static final ItemStatus UNSUBSCRIPTION_PENDING = unsubscribe;
public static ItemStatus fromString(String s) {
if (s == null) {
return null;
}
try {
return ItemStatus.valueOf(s);
}
catch (IllegalArgumentException e) {
return null;
}
}
}
public static enum ItemType {
/**
* The user does not have a subscription to the contact's presence, and the contact does not
* have a subscription to the user's presence; this is the default value, so if the
* subscription attribute is not included then the state is to be understood as "none".
*/
none,
/**
* The user has a subscription to the contact's presence, but the contact does not have a
* subscription to the user's presence.
*/
to,
/**
* The contact has a subscription to the user's presence, but the user does not have a
* subscription to the contact's presence.
*/
from,
/**
* The user and the contact have subscriptions to each other's presence (also called a
* "mutual subscription").
*/
both,
/**
* The user wishes to stop receiving presence updates from the subscriber.
*/
remove
}
}

View file

@ -0,0 +1,49 @@
/**
*
* Copyright © 2014 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.roster.packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.util.XmlStringBuilder;
public class RosterVer implements PacketExtension {
public static final String ELEMENT = "ver";
public static final String NAMESPACE = "urn:xmpp:features:rosterver";
public static final RosterVer INSTANCE = new RosterVer();
private RosterVer() {
}
@Override
public String getElementName() {
return ELEMENT;
}
@Override
public String getNamespace() {
return NAMESPACE;
}
@Override
public XmlStringBuilder toXML() {
XmlStringBuilder xml = new XmlStringBuilder(this);
xml.closeEmptyElement();
return xml;
}
}

View file

@ -0,0 +1,83 @@
/**
*
* Copyright © 2003-2007 Jive Software, 2014-2015 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.roster.provider;
import java.io.IOException;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.provider.IQProvider;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
public class RosterPacketProvider extends IQProvider<RosterPacket> {
public static final RosterPacketProvider INSTANCE = new RosterPacketProvider();
@Override
public RosterPacket parse(XmlPullParser parser, int initialDepth) throws XmlPullParserException, IOException,
SmackException {
RosterPacket roster = new RosterPacket();
RosterPacket.Item item = null;
String version = parser.getAttributeValue("", "ver");
roster.setVersion(version);
outerloop: while (true) {
int eventType = parser.next();
switch(eventType) {
case XmlPullParser.START_TAG:
String startTag = parser.getName();
switch (startTag) {
case "item":
String jid = parser.getAttributeValue("", "jid");
String name = parser.getAttributeValue("", "name");
// Create packet.
item = new RosterPacket.Item(jid, name);
// Set status.
String ask = parser.getAttributeValue("", "ask");
RosterPacket.ItemStatus status = RosterPacket.ItemStatus.fromString(ask);
item.setItemStatus(status);
// Set type.
String subscription = parser.getAttributeValue("", "subscription");
RosterPacket.ItemType type = RosterPacket.ItemType.valueOf(subscription != null ? subscription : "none");
item.setItemType(type);
break;
case "group":
// TODO item!= null
final String groupName = parser.nextText();
if (groupName != null && groupName.trim().length() > 0) {
item.addGroupName(groupName);
}
break;
}
break;
case XmlPullParser.END_TAG:
String endTag = parser.getName();
switch(endTag) {
case "item":
roster.addRosterItem(item);
break;
case "query":
break outerloop;
}
}
}
return roster;
}
}

View file

@ -0,0 +1,35 @@
/**
*
* Copyright © 2015 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.roster.provider;
import java.io.IOException;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.provider.PacketExtensionProvider;
import org.jivesoftware.smack.roster.packet.RosterVer;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
public class RosterVerStreamFeatureProvider extends PacketExtensionProvider<RosterVer> {
@Override
public RosterVer parse(XmlPullParser parser, int initialDepth)
throws XmlPullParserException, IOException, SmackException {
return RosterVer.INSTANCE;
}
}

View file

@ -0,0 +1,308 @@
/**
*
* Copyright 2013 the original author or authors
*
* 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.roster.rosterstore;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jivesoftware.smack.roster.packet.RosterPacket.Item;
import org.jivesoftware.smack.util.FileUtils;
import org.jivesoftware.smack.util.XmlStringBuilder;
import org.jivesoftware.smack.util.stringencoder.Base32;
import org.xmlpull.v1.XmlPullParserFactory;
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 DirectoryRosterStore 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 Logger LOGGER = Logger.getLogger(DirectoryRosterStore.class.getName());
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 DirectoryRosterStore(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 DirectoryRosterStore} instance if successful,
* <code>null</code> else.
*/
public static DirectoryRosterStore init(final File baseDir) {
DirectoryRosterStore store = new DirectoryRosterStore(baseDir);
if (store.setRosterVersion("")) {
return store;
}
else {
return null;
}
}
/**
* Opens a roster store
* @param baseDir
* The directory containing the roster store.
* @return A {@link DirectoryRosterStore} instance if successful,
* <code>null</code> else.
*/
public static DirectoryRosterStore open(final File baseDir) {
DirectoryRosterStore store = new DirectoryRosterStore(baseDir);
String s = FileUtils.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 = FileUtils.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 FileUtils.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) {
return getBareJidFile(bareJid).delete() && setRosterVersion(version);
}
@Override
public boolean resetEntries(Collection<Item> items, String version) {
for (File file : fileDir.listFiles(rosterDirFilter)) {
file.delete();
}
for (Item item : items) {
if (!addEntryRaw(item)) {
return false;
}
}
return setRosterVersion(version);
}
private Item readEntry(File file) {
String s = FileUtils.readFile(file);
if (s == null) {
return null;
}
String parserName;
String user = null;
String name = null;
String type = null;
String status = null;
List<String> groupNames = new ArrayList<String>();
try {
XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
parser.setInput(new StringReader(s));
boolean done = false;
while (!done) {
int eventType = parser.next();
parserName = parser.getName();
if (eventType == XmlPullParser.START_TAG) {
if (parserName.equals("item")) {
user = name = type = status = null;
}
else if (parserName.equals("user")) {
parser.next();
user = parser.getText();
}
else if (parserName.equals("name")) {
parser.next();
name = parser.getText();
}
else if (parserName.equals("type")) {
parser.next();
type = parser.getText();
}
else if (parserName.equals("status")) {
parser.next();
status = parser.getText();
}
else if (parserName.equals("group")) {
parser.next();
parser.next();
String group = parser.getText();
if (group != null) {
groupNames.add(group);
}
else {
log("Invalid group entry in store entry file "
+ file);
}
}
}
else if (eventType == XmlPullParser.END_TAG) {
if (parserName.equals("item")) {
done = true;
}
}
}
}
catch (IOException e) {
LOGGER.log(Level.SEVERE, "readEntry()", e);
return null;
}
catch (XmlPullParserException e) {
log("Invalid group entry in store entry file "
+ file);
LOGGER.log(Level.SEVERE, "readEntry()", e);
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) {
XmlStringBuilder xml = new XmlStringBuilder();
xml.openElement("item");
xml.element("user", item.getUser());
xml.optElement("name", item.getName());
xml.optElement("type", item.getItemType());
xml.optElement("status", item.getItemStatus());
for (String groupName : item.getGroupNames()) {
xml.openElement("group");
xml.element("groupName", groupName);
xml.closeElement("group");
}
xml.closeElement("item");
return FileUtils.writeFile(getBareJidFile(item.getUser()), xml.toString());
}
private File getBareJidFile(String bareJid) {
String encodedJid = Base32.encode(bareJid);
return new File(fileDir, ENTRY_PREFIX + encodedJid);
}
private void log(String error) {
System.err.println(error);
}
}

View file

@ -0,0 +1,69 @@
/**
*
* Copyright the original author or authors
*
* 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.roster.rosterstore;
import java.util.Collection;
import org.jivesoftware.smack.roster.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 org.jivesoftware.smack.roster.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 org.jivesoftware.smack.roster.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);
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0"?>
<smackProviders>
<iqProvider>
<elementName>query</elementName>
<namespace>jabber:iq:roster</namespace>
<className>org.jivesoftware.smack.roster.provider.RosterPacketProvider</className>
</iqProvider>
<streamFeatureProvider>
<elementName>ver</elementName>
<namespace>urn:xmpp:features:rosterver</namespace>
<className>org.jivesoftware.smack.roster.provider.RosterVerStreamFeatureProvider</className>
</streamFeatureProvider>
</smackProviders>

View file

@ -0,0 +1,5 @@
<smack>
<startupClasses>
<className>org.jivesoftware.smack.roster.Roster</className>
</startupClasses>
</smack>

View file

@ -0,0 +1,401 @@
/**
*
* Copyright 2010 Jive Software.
*
* 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.chat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.jivesoftware.smack.DummyConnection;
import org.jivesoftware.smack.chat.ChatManager.MatchMode;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Message.Type;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.test.util.WaitForPacketListener;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
public class ChatConnectionTest {
private DummyConnection dc;
private ChatManager cm;
private TestChatManagerListener listener;
private WaitForPacketListener waitListener;
@Before
public void setUp() throws Exception {
// Defaults
ChatManager.setDefaultIsNormalIncluded(true);
ChatManager.setDefaultMatchMode(MatchMode.BARE_JID);
dc = DummyConnection.newConnectedDummyConnection();
cm = ChatManager.getInstanceFor(dc);
listener = new TestChatManagerListener();
cm.addChatListener(listener);
waitListener = new WaitForPacketListener();
dc.addSyncPacketListener(waitListener, null);
}
@After
public void tearDown() throws Exception {
if (dc != null) {
dc.disconnect();
}
}
@Test
public void validateDefaultSetNormalIncludedFalse() {
ChatManager.setDefaultIsNormalIncluded(false);
assertFalse(ChatManager.getInstanceFor(new DummyConnection()).isNormalIncluded());
}
@Test
public void validateDefaultSetNormalIncludedTrue() {
ChatManager.setDefaultIsNormalIncluded(true);
assertTrue(ChatManager.getInstanceFor(new DummyConnection()).isNormalIncluded());
}
@Test
public void validateDefaultSetMatchModeNone() {
ChatManager.setDefaultMatchMode(MatchMode.NONE);
assertEquals(MatchMode.NONE, ChatManager.getInstanceFor(new DummyConnection()).getMatchMode());
}
@Test
public void validateDefaultSetMatchModeBareJid() {
ChatManager.setDefaultMatchMode(MatchMode.BARE_JID);
assertEquals(MatchMode.BARE_JID, ChatManager.getInstanceFor(new DummyConnection()).getMatchMode());
}
@Test
public void validateMessageTypeWithDefaults1() {
Message incomingChat = createChatPacket("134", true);
incomingChat.setType(Type.chat);
processServerMessage(incomingChat);
assertNotNull(listener.getNewChat());
}
@Test
public void validateMessageTypeWithDefaults2() {
Message incomingChat = createChatPacket("134", true);
incomingChat.setType(Type.normal);
processServerMessage(incomingChat);
assertNotNull(listener.getNewChat());
}
@Test
public void validateMessageTypeWithDefaults3() {
Message incomingChat = createChatPacket("134", true);
incomingChat.setType(Type.groupchat);
processServerMessage(incomingChat);
assertNull(listener.getNewChat());
}
@Test
public void validateMessageTypeWithDefaults4() {
Message incomingChat = createChatPacket("134", true);
incomingChat.setType(Type.headline);
assertNull(listener.getNewChat());
}
@Test
public void validateMessageTypeWithNoNormal1() {
cm.setNormalIncluded(false);
Message incomingChat = createChatPacket("134", true);
incomingChat.setType(Type.chat);
processServerMessage(incomingChat);
assertNotNull(listener.getNewChat());
}
@Test
public void validateMessageTypeWithNoNormal2() {
cm.setNormalIncluded(false);
Message incomingChat = createChatPacket("134", true);
incomingChat.setType(Type.normal);
processServerMessage(incomingChat);
assertNull(listener.getNewChat());
}
// No thread behaviour
@Test
public void chatMatchedOnJIDWhenNoThreadBareMode() {
// MatchMode.BARE_JID is the default, so setting required.
TestMessageListener msgListener = new TestMessageListener();
TestChatManagerListener listener = new TestChatManagerListener(msgListener);
cm.addChatListener(listener);
Packet incomingChat = createChatPacket(null, true);
processServerMessage(incomingChat);
Chat newChat = listener.getNewChat();
assertNotNull(newChat);
// Should match on chat with full jid
incomingChat = createChatPacket(null, true);
processServerMessage(incomingChat);
assertEquals(2, msgListener.getNumMessages());
// Should match on chat with bare jid
incomingChat = createChatPacket(null, false);
processServerMessage(incomingChat);
assertEquals(3, msgListener.getNumMessages());
}
@Test
public void chatMatchedOnJIDWhenNoThreadJidMode() {
TestMessageListener msgListener = new TestMessageListener();
TestChatManagerListener listener = new TestChatManagerListener(msgListener);
cm.setMatchMode(MatchMode.SUPPLIED_JID);
cm.addChatListener(listener);
Packet incomingChat = createChatPacket(null, true);
processServerMessage(incomingChat);
Chat newChat = listener.getNewChat();
assertNotNull(newChat);
cm.removeChatListener(listener);
// Should match on chat with full jid
incomingChat = createChatPacket(null, true);
processServerMessage(incomingChat);
assertEquals(2, msgListener.getNumMessages());
// Should not match on chat with bare jid
TestChatManagerListener listener2 = new TestChatManagerListener();
cm.addChatListener(listener2);
incomingChat = createChatPacket(null, false);
processServerMessage(incomingChat);
assertEquals(2, msgListener.getNumMessages());
assertNotNull(listener2.getNewChat());
}
@Test
public void chatMatchedOnJIDWhenNoThreadNoneMode() {
TestMessageListener msgListener = new TestMessageListener();
TestChatManagerListener listener = new TestChatManagerListener(msgListener);
cm.setMatchMode(MatchMode.NONE);
cm.addChatListener(listener);
Packet incomingChat = createChatPacket(null, true);
processServerMessage(incomingChat);
Chat newChat = listener.getNewChat();
assertNotNull(newChat);
assertEquals(1, msgListener.getNumMessages());
cm.removeChatListener(listener);
// Should not match on chat with full jid
TestChatManagerListener listener2 = new TestChatManagerListener();
cm.addChatListener(listener2);
incomingChat = createChatPacket(null, true);
processServerMessage(incomingChat);
assertEquals(1, msgListener.getNumMessages());
assertNotNull(newChat);
cm.removeChatListener(listener2);
// Should not match on chat with bare jid
TestChatManagerListener listener3 = new TestChatManagerListener();
cm.addChatListener(listener3);
incomingChat = createChatPacket(null, false);
processServerMessage(incomingChat);
assertEquals(1, msgListener.getNumMessages());
assertNotNull(listener3.getNewChat());
}
/**
* Confirm that an existing chat created with a base jid is matched to an incoming chat message that has no thread
* id and the user is a full jid.
*/
@Test
public void chatFoundWhenNoThreadFullJid() {
Chat outgoing = cm.createChat("you@testserver", null);
Packet incomingChat = createChatPacket(null, true);
processServerMessage(incomingChat);
Chat newChat = listener.getNewChat();
assertNotNull(newChat);
assertTrue(newChat == outgoing);
}
/**
* Confirm that an existing chat created with a base jid is matched to an incoming chat message that has no thread
* id and the user is a base jid.
*/
@Test
public void chatFoundWhenNoThreadBaseJid() {
Chat outgoing = cm.createChat("you@testserver", null);
Packet incomingChat = createChatPacket(null, false);
processServerMessage(incomingChat);
Chat newChat = listener.getNewChat();
assertNotNull(newChat);
assertTrue(newChat == outgoing);
}
/**
* Confirm that an existing chat created with a base jid is matched to an incoming chat message that has the same id
* and the user is a full jid.
*/
@Test
public void chatFoundWithSameThreadFullJid() {
Chat outgoing = cm.createChat("you@testserver", null);
Packet incomingChat = createChatPacket(outgoing.getThreadID(), true);
processServerMessage(incomingChat);
Chat newChat = listener.getNewChat();
assertNotNull(newChat);
assertTrue(newChat == outgoing);
}
/**
* Confirm that an existing chat created with a base jid is matched to an incoming chat message that has the same id
* and the user is a base jid.
*/
@Test
public void chatFoundWithSameThreadBaseJid() {
Chat outgoing = cm.createChat("you@testserver", null);
Packet incomingChat = createChatPacket(outgoing.getThreadID(), false);
processServerMessage(incomingChat);
Chat newChat = listener.getNewChat();
assertNotNull(newChat);
assertTrue(newChat == outgoing);
}
/**
* Confirm that an existing chat created with a base jid is not matched to an incoming chat message that has a
* different id and the same user as a base jid.
*/
@Test
public void chatNotFoundWithDiffThreadBaseJid() {
Chat outgoing = cm.createChat("you@testserver", null);
Packet incomingChat = createChatPacket(outgoing.getThreadID() + "ff", false);
processServerMessage(incomingChat);
Chat newChat = listener.getNewChat();
assertNotNull(newChat);
assertFalse(newChat == outgoing);
}
/**
* Confirm that an existing chat created with a base jid is not matched to an incoming chat message that has a
* different id and the same base jid.
*/
@Test
public void chatNotFoundWithDiffThreadFullJid() {
Chat outgoing = cm.createChat("you@testserver", null);
Packet incomingChat = createChatPacket(outgoing.getThreadID() + "ff", true);
processServerMessage(incomingChat);
Chat newChat = listener.getNewChat();
assertNotNull(newChat);
assertFalse(newChat == outgoing);
}
@Test
public void chatNotMatchedWithTypeNormal() {
cm.setNormalIncluded(false);
Message incomingChat = createChatPacket(null, false);
incomingChat.setType(Type.normal);
processServerMessage(incomingChat);
assertNull(listener.getNewChat());
}
private Message createChatPacket(final String threadId, final boolean isFullJid) {
Message chatMsg = new Message("me@testserver", Message.Type.chat);
chatMsg.setBody("the body message - " + System.currentTimeMillis());
chatMsg.setFrom("you@testserver" + (isFullJid ? "/resource" : ""));
chatMsg.setThread(threadId);
return chatMsg;
}
private void processServerMessage(Packet incomingChat) {
TestChatServer chatServer = new TestChatServer(incomingChat, dc);
chatServer.start();
try {
chatServer.join();
} catch (InterruptedException e) {
fail();
}
waitListener.waitAndReset();
}
class TestChatManagerListener extends WaitForPacketListener implements ChatManagerListener {
private Chat newChat;
private ChatMessageListener listener;
public TestChatManagerListener(TestMessageListener msgListener) {
listener = msgListener;
}
public TestChatManagerListener() {
}
@Override
public void chatCreated(Chat chat, boolean createdLocally) {
newChat = chat;
if (listener != null)
newChat.addMessageListener(listener);
reportInvoked();
}
public Chat getNewChat() {
return newChat;
}
}
private class TestChatServer extends Thread {
private Packet chatPacket;
private DummyConnection con;
TestChatServer(Packet chatMsg, DummyConnection conect) {
chatPacket = chatMsg;
con = conect;
}
@Override
public void run() {
con.processPacket(chatPacket);
}
}
private class TestMessageListener implements ChatMessageListener {
private Chat msgChat;
private int counter = 0;
@Override
public void processMessage(Chat chat, Message message) {
msgChat = chat;
counter++;
}
@SuppressWarnings("unused")
public Chat getChat() {
return msgChat;
}
public int getNumMessages() {
return counter;
}
}
}

View file

@ -0,0 +1,25 @@
/**
*
* Copyright 2015 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.im;
public class InitSmackIm {
static {
(new SmackImInitializer()).initialize();
}
}

View file

@ -0,0 +1,34 @@
/**
*
* Copyright 2015 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.im;
import static org.junit.Assert.assertTrue;
import java.util.List;
import org.junit.Test;
public class SmackImInitializerTest {
@Test
public void testExtensionInitializer() {
SmackImInitializer initializer = new SmackImInitializer();
List<Exception> exceptions = initializer.initialize();
assertTrue(exceptions.size() == 0);
}
}

View file

@ -0,0 +1,63 @@
/**
*
* Copyright the original author or authors
*
* 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.roster;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import org.jivesoftware.smack.DummyConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.junit.Before;
import org.junit.Test;
/**
* Tests the behavior of the roster if the connection is not authenticated yet.
*
* @author Henning Staib
*/
public class RosterOfflineTest {
DummyConnection connection;
Roster roster;
@Before
public void setup() throws XMPPException, SmackException {
this.connection = new DummyConnection();
assertFalse(connection.isConnected());
roster = Roster.getInstanceFor(connection);
assertNotNull(roster);
}
@Test(expected = SmackException.class)
public void shouldThrowExceptionOnCreateEntry() throws Exception {
roster.createEntry("test", "test", null);
}
@Test(expected = SmackException.class)
public void shouldThrowExceptionOnReload() throws Exception {
roster.reload();
}
@Test(expected = SmackException.class)
public void shouldThrowExceptionRemoveEntry() throws Exception {
roster.removeEntry(null);
}
}

View file

@ -0,0 +1,785 @@
/**
*
* Copyright 2010 Jive Software.
*
* 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.roster;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
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 static org.junit.Assert.fail;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.jivesoftware.smack.DummyConnection;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.im.InitSmackIm;
import org.jivesoftware.smack.packet.ErrorIQ;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smack.packet.XMPPError.Condition;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jivesoftware.smack.roster.packet.RosterPacket.Item;
import org.jivesoftware.smack.roster.packet.RosterPacket.ItemType;
import org.jivesoftware.smack.test.util.TestUtils;
import org.jivesoftware.smack.test.util.WaitForPacketListener;
import org.jivesoftware.smack.util.PacketParserUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.xmlpull.v1.XmlPullParser;
/**
* Tests that verifies the correct behavior of the {@link Roster} implementation.
*
* @see Roster
* @see <a href="http://xmpp.org/rfcs/rfc3921.html#roster">Roster Management</a>
* @author Guenther Niess
*/
public class RosterTest extends InitSmackIm {
private DummyConnection connection;
private Roster roster;
private TestRosterListener rosterListener;
@Before
public void setUp() throws Exception {
connection = new DummyConnection();
connection.connect();
connection.login();
rosterListener = new TestRosterListener();
roster = Roster.getInstanceFor(connection);
roster.addRosterListener(rosterListener);
connection.setPacketReplyTimeout(1000 * 60 * 5);
}
@After
public void tearDown() throws Exception {
if (connection != null) {
if (rosterListener != null) {
roster.removeRosterListener(rosterListener);
rosterListener = null;
}
connection.disconnect();
connection = null;
}
}
/**
* Test a simple roster initialization according to the example in
* <a href="http://xmpp.org/rfcs/rfc3921.html#roster-login"
* >RFC3921: Retrieving One's Roster on Login</a>.
*/
@Test
public void testSimpleRosterInitialization() throws Exception {
assertNotNull("Can't get the roster from the provided connection!", roster);
assertFalse("Roster shouldn't be already loaded!",
roster.isLoaded());
// Perform roster initialization
initRoster();
// Verify roster
assertTrue("Roster can't be loaded!", roster.waitUntilLoaded());
verifyRomeosEntry(roster.getEntry("romeo@example.net"));
verifyMercutiosEntry(roster.getEntry("mercutio@example.com"));
verifyBenvoliosEntry(roster.getEntry("benvolio@example.net"));
assertSame("Wrong number of roster entries.", 3, roster.getEntries().size());
// Verify roster listener
assertTrue("The roster listener wasn't invoked for Romeo.",
rosterListener.getAddedAddresses().contains("romeo@example.net"));
assertTrue("The roster listener wasn't invoked for Mercutio.",
rosterListener.getAddedAddresses().contains("mercutio@example.com"));
assertTrue("The roster listener wasn't invoked for Benvolio.",
rosterListener.getAddedAddresses().contains("benvolio@example.net"));
assertSame("RosterListeners implies that a item was deleted!",
0,
rosterListener.getDeletedAddresses().size());
assertSame("RosterListeners implies that a item was updated!",
0,
rosterListener.getUpdatedAddresses().size());
}
/**
* Test adding a roster item according to the example in
* <a href="http://xmpp.org/rfcs/rfc3921.html#roster-add"
* >RFC3921: Adding a Roster Item</a>.
*/
@Test
public void testAddRosterItem() throws Throwable {
// Constants for the new contact
final String contactJID = "nurse@example.com";
final String contactName = "Nurse";
final String[] contactGroup = {"Servants"};
// Setup
assertNotNull("Can't get the roster from the provided connection!", roster);
initRoster();
rosterListener.reset();
// Adding the new roster item
final RosterUpdateResponder serverSimulator = new RosterUpdateResponder() {
void verifyUpdateRequest(final RosterPacket updateRequest) {
final Item item = updateRequest.getRosterItems().iterator().next();
assertSame("The provided JID doesn't match the requested!",
contactJID,
item.getUser());
assertSame("The provided name doesn't match the requested!",
contactName,
item.getName());
assertSame("The provided group number doesn't match the requested!",
contactGroup.length,
item.getGroupNames().size());
assertSame("The provided group doesn't match the requested!",
contactGroup[0],
item.getGroupNames().iterator().next());
}
};
serverSimulator.start();
roster.createEntry(contactJID, contactName, contactGroup);
serverSimulator.join();
// Check if an error occurred within the simulator
final Throwable exception = serverSimulator.getException();
if (exception != null) {
throw exception;
}
rosterListener.waitUntilInvocationOrTimeout();
// Verify the roster entry of the new contact
final RosterEntry addedEntry = roster.getEntry(contactJID);
assertNotNull("The new contact wasn't added to the roster!", addedEntry);
assertTrue("The roster listener wasn't invoked for the new contact!",
rosterListener.getAddedAddresses().contains(contactJID));
assertSame("Setup wrong name for the new contact!",
contactName,
addedEntry.getName());
assertSame("Setup wrong default subscription status!",
ItemType.none,
addedEntry.getType());
assertSame("The new contact should be member of exactly one group!",
1,
addedEntry.getGroups().size());
assertSame("Setup wrong group name for the added contact!",
contactGroup[0],
addedEntry.getGroups().iterator().next().getName());
// Verify the unchanged roster items
verifyRomeosEntry(roster.getEntry("romeo@example.net"));
verifyMercutiosEntry(roster.getEntry("mercutio@example.com"));
verifyBenvoliosEntry(roster.getEntry("benvolio@example.net"));
assertSame("Wrong number of roster entries.", 4, roster.getEntries().size());
}
/**
* Test updating a roster item according to the example in
* <a href="http://xmpp.org/rfcs/rfc3921.html#roster-update"
* >RFC3921: Updating a Roster Item</a>.
*/
@Test
public void testUpdateRosterItem() throws Throwable {
// Constants for the updated contact
final String contactJID = "romeo@example.net";
final String contactName = "Romeo";
final String[] contactGroups = {"Friends", "Lovers"};
// Setup
assertNotNull("Can't get the roster from the provided connection!", roster);
initRoster();
rosterListener.reset();
// Updating the roster item
final RosterUpdateResponder serverSimulator = new RosterUpdateResponder() {
void verifyUpdateRequest(final RosterPacket updateRequest) {
final Item item = updateRequest.getRosterItems().iterator().next();
assertSame("The provided JID doesn't match the requested!",
contactJID,
item.getUser());
assertSame("The provided name doesn't match the requested!",
contactName,
item.getName());
assertTrue("The updated contact doesn't belong to the requested groups ("
+ contactGroups[0] +")!",
item.getGroupNames().contains(contactGroups[0]));
assertTrue("The updated contact doesn't belong to the requested groups ("
+ contactGroups[1] +")!",
item.getGroupNames().contains(contactGroups[1]));
assertSame("The provided group number doesn't match the requested!",
contactGroups.length,
item.getGroupNames().size());
}
};
serverSimulator.start();
roster.createGroup(contactGroups[1]).addEntry(roster.getEntry(contactJID));
serverSimulator.join();
// Check if an error occurred within the simulator
final Throwable exception = serverSimulator.getException();
if (exception != null) {
throw exception;
}
rosterListener.waitUntilInvocationOrTimeout();
// Verify the roster entry of the updated contact
final RosterEntry addedEntry = roster.getEntry(contactJID);
assertNotNull("The contact was deleted from the roster!", addedEntry);
assertTrue("The roster listener wasn't invoked for the updated contact!",
rosterListener.getUpdatedAddresses().contains(contactJID));
assertSame("Setup wrong name for the changed contact!",
contactName,
addedEntry.getName());
assertTrue("The updated contact doesn't belong to the requested groups ("
+ contactGroups[0] +")!",
roster.getGroup(contactGroups[0]).contains(addedEntry));
assertTrue("The updated contact doesn't belong to the requested groups ("
+ contactGroups[1] +")!",
roster.getGroup(contactGroups[1]).contains(addedEntry));
assertSame("The updated contact should be member of two groups!",
contactGroups.length,
addedEntry.getGroups().size());
// Verify the unchanged roster items
verifyMercutiosEntry(roster.getEntry("mercutio@example.com"));
verifyBenvoliosEntry(roster.getEntry("benvolio@example.net"));
assertSame("Wrong number of roster entries (" + roster.getEntries() + ").",
3,
roster.getEntries().size());
}
/**
* Test deleting a roster item according to the example in
* <a href="http://xmpp.org/rfcs/rfc3921.html#roster-delete"
* >RFC3921: Deleting a Roster Item</a>.
*/
@Test
public void testDeleteRosterItem() throws Throwable {
// The contact which should be deleted
final String contactJID = "romeo@example.net";
// Setup
assertNotNull("Can't get the roster from the provided connection!", roster);
initRoster();
rosterListener.reset();
// Delete a roster item
final RosterUpdateResponder serverSimulator = new RosterUpdateResponder() {
void verifyUpdateRequest(final RosterPacket updateRequest) {
final Item item = updateRequest.getRosterItems().iterator().next();
assertSame("The provided JID doesn't match the requested!",
contactJID,
item.getUser());
}
};
serverSimulator.start();
roster.removeEntry(roster.getEntry(contactJID));
serverSimulator.join();
// Check if an error occurred within the simulator
final Throwable exception = serverSimulator.getException();
if (exception != null) {
throw exception;
}
rosterListener.waitUntilInvocationOrTimeout();
// Verify
final RosterEntry deletedEntry = roster.getEntry(contactJID);
assertNull("The contact wasn't deleted from the roster!", deletedEntry);
assertTrue("The roster listener wasn't invoked for the deleted contact!",
rosterListener.getDeletedAddresses().contains(contactJID));
verifyMercutiosEntry(roster.getEntry("mercutio@example.com"));
verifyBenvoliosEntry(roster.getEntry("benvolio@example.net"));
assertSame("Wrong number of roster entries (" + roster.getEntries() + ").",
2,
roster.getEntries().size());
}
/**
* Test a simple roster push according to the example in
* <a href="http://xmpp.org/internet-drafts/draft-ietf-xmpp-3921bis-03.html#roster-syntax-actions-push"
* >RFC3921bis-03: Roster Push</a>.
*/
@Test
public void testSimpleRosterPush() throws Throwable {
final String contactJID = "nurse@example.com";
assertNotNull("Can't get the roster from the provided connection!", roster);
final StringBuilder sb = new StringBuilder();
sb.append("<iq id=\"rostertest1\" type=\"set\" ")
.append("to=\"").append(connection.getUser()).append("\">")
.append("<query xmlns=\"jabber:iq:roster\">")
.append("<item jid=\"").append(contactJID).append("\"/>")
.append("</query>")
.append("</iq>");
final XmlPullParser parser = TestUtils.getIQParser(sb.toString());
final IQ rosterPush = PacketParserUtils.parseIQ(parser);
initRoster();
rosterListener.reset();
// Simulate receiving the roster push
connection.processPacket(rosterPush);
rosterListener.waitUntilInvocationOrTimeout();
// Verify the roster entry of the new contact
final RosterEntry addedEntry = roster.getEntry(contactJID);
assertNotNull("The new contact wasn't added to the roster!", addedEntry);
assertTrue("The roster listener wasn't invoked for the new contact!",
rosterListener.getAddedAddresses().contains(contactJID));
assertSame("Setup wrong default subscription status!",
ItemType.none,
addedEntry.getType());
assertSame("The new contact shouldn't be member of any group!",
0,
addedEntry.getGroups().size());
// Verify the unchanged roster items
verifyRomeosEntry(roster.getEntry("romeo@example.net"));
verifyMercutiosEntry(roster.getEntry("mercutio@example.com"));
verifyBenvoliosEntry(roster.getEntry("benvolio@example.net"));
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
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!"));
final String requestId = packet.getPacketID();
// Simulate receiving the roster push
connection.processPacket(packet);
// Smack should reply with an error IQ
ErrorIQ errorIQ = (ErrorIQ) connection.getSentPacket();
assertEquals(requestId, errorIQ.getPacketID());
assertEquals(Condition.service_unavailable, errorIQ.getError().getCondition());
assertNull("Contact was added to roster", Roster.getInstanceFor(connection).getEntry("spam@example.com"));
}
/**
* Test if adding an user with an empty group is equivalent with providing
* no group.
*
* @see <a href="http://www.igniterealtime.org/issues/browse/SMACK-294">SMACK-294</a>
*/
@Test(timeout=5000)
public void testAddEmptyGroupEntry() throws Throwable {
// Constants for the new contact
final String contactJID = "nurse@example.com";
final String contactName = "Nurse";
final String[] contactGroup = {""};
// Setup
assertNotNull("Can't get the roster from the provided connection!", roster);
initRoster();
rosterListener.reset();
// Adding the new roster item
final RosterUpdateResponder serverSimulator = new RosterUpdateResponder() {
void verifyUpdateRequest(final RosterPacket updateRequest) {
final Item item = updateRequest.getRosterItems().iterator().next();
assertSame("The provided JID doesn't match the requested!",
contactJID,
item.getUser());
assertSame("The provided name doesn't match the requested!",
contactName,
item.getName());
assertSame("Shouldn't provide an empty group element!",
0,
item.getGroupNames().size());
}
};
serverSimulator.start();
roster.createEntry(contactJID, contactName, contactGroup);
serverSimulator.join();
// Check if an error occurred within the simulator
final Throwable exception = serverSimulator.getException();
if (exception != null) {
throw exception;
}
rosterListener.waitUntilInvocationOrTimeout();
// Verify the roster entry of the new contact
final RosterEntry addedEntry = roster.getEntry(contactJID);
assertNotNull("The new contact wasn't added to the roster!", addedEntry);
assertTrue("The roster listener wasn't invoked for the new contact!",
rosterListener.getAddedAddresses().contains(contactJID));
assertSame("Setup wrong name for the new contact!",
contactName,
addedEntry.getName());
assertSame("Setup wrong default subscription status!",
ItemType.none,
addedEntry.getType());
assertSame("The new contact shouldn't be member of any group!",
0,
addedEntry.getGroups().size());
// Verify the unchanged roster items
verifyRomeosEntry(roster.getEntry("romeo@example.net"));
verifyMercutiosEntry(roster.getEntry("mercutio@example.com"));
verifyBenvoliosEntry(roster.getEntry("benvolio@example.net"));
assertSame("Wrong number of roster entries.", 4, roster.getEntries().size());
}
/**
* Test processing a roster push with an empty group is equivalent with providing
* no group.
*
* @see <a href="http://www.igniterealtime.org/issues/browse/SMACK-294">SMACK-294</a>
*/
@Test
public void testEmptyGroupRosterPush() throws Throwable {
final String contactJID = "nurse@example.com";
assertNotNull("Can't get the roster from the provided connection!", roster);
final StringBuilder sb = new StringBuilder();
sb.append("<iq id=\"rostertest2\" type=\"set\" ")
.append("to=\"").append(connection.getUser()).append("\">")
.append("<query xmlns=\"jabber:iq:roster\">")
.append("<item jid=\"").append(contactJID).append("\">")
.append("<group></group>")
.append("</item>")
.append("</query>")
.append("</iq>");
final XmlPullParser parser = TestUtils.getIQParser(sb.toString());
final IQ rosterPush = PacketParserUtils.parseIQ(parser);
initRoster();
rosterListener.reset();
// Simulate receiving the roster push
connection.processPacket(rosterPush);
rosterListener.waitUntilInvocationOrTimeout();
// Verify the roster entry of the new contact
final RosterEntry addedEntry = roster.getEntry(contactJID);
assertNotNull("The new contact wasn't added to the roster!", addedEntry);
assertTrue("The roster listener wasn't invoked for the new contact!",
rosterListener.getAddedAddresses().contains(contactJID));
assertSame("Setup wrong default subscription status!",
ItemType.none,
addedEntry.getType());
assertSame("The new contact shouldn't be member of any group!",
0,
addedEntry.getGroups().size());
// Verify the unchanged roster items
verifyRomeosEntry(roster.getEntry("romeo@example.net"));
verifyMercutiosEntry(roster.getEntry("mercutio@example.com"));
verifyBenvoliosEntry(roster.getEntry("benvolio@example.net"));
assertSame("Wrong number of roster entries.", 4, roster.getEntries().size());
}
/**
* Remove all roster entries by iterating trough {@link Roster#getEntries()}
* and simulating receiving roster pushes from the server.
*
* @param connection the dummy connection of which the provided roster belongs to.
* @param roster the roster (or buddy list) which should be initialized.
*/
public static void removeAllRosterEntries(DummyConnection connection, Roster roster)
throws InterruptedException, XMPPException {
for(RosterEntry entry : roster.getEntries()) {
// prepare the roster push packet
final RosterPacket rosterPush= new RosterPacket();
rosterPush.setType(Type.set);
rosterPush.setTo(connection.getUser());
// prepare the buddy's item entry which should be removed
final RosterPacket.Item item = new RosterPacket.Item(entry.getUser(), entry.getName());
item.setItemType(ItemType.remove);
rosterPush.addRosterItem(item);
// simulate receiving the roster push
connection.processPacket(rosterPush);
}
}
/**
* Initialize the roster according to the example in
* <a href="http://xmpp.org/rfcs/rfc3921.html#roster-login"
* >RFC3921: Retrieving One's Roster on Login</a>.
*
* @param connection the dummy connection of which the provided roster belongs to.
* @param roster the roster (or buddy list) which should be initialized.
* @throws SmackException
*/
private void initRoster() throws InterruptedException, XMPPException, SmackException {
roster.reload();
while (true) {
final Packet sentPacket = connection.getSentPacket();
if (sentPacket instanceof RosterPacket && ((IQ) sentPacket).getType() == Type.get) {
// setup the roster get request
final RosterPacket rosterRequest = (RosterPacket) sentPacket;
assertSame("The <query/> element MUST NOT contain any <item/> child elements!",
0,
rosterRequest.getRosterItemCount());
// prepare the roster result
final RosterPacket rosterResult = new RosterPacket();
rosterResult.setTo(connection.getUser());
rosterResult.setType(Type.result);
rosterResult.setPacketID(rosterRequest.getPacketID());
// prepare romeo's roster entry
final Item romeo = new Item("romeo@example.net", "Romeo");
romeo.addGroupName("Friends");
romeo.setItemType(ItemType.both);
rosterResult.addRosterItem(romeo);
// prepare mercutio's roster entry
final Item mercutio = new Item("mercutio@example.com", "Mercutio");
mercutio.setItemType(ItemType.from);
rosterResult.addRosterItem(mercutio);
// prepare benvolio's roster entry
final Item benvolio = new Item("benvolio@example.net", "Benvolio");
benvolio.setItemType(ItemType.both);
rosterResult.addRosterItem(benvolio);
// simulate receiving the roster result and exit the loop
connection.processPacket(rosterResult);
break;
}
}
roster.waitUntilLoaded();
rosterListener.waitUntilInvocationOrTimeout();
}
/**
* Check Romeo's roster entry according to the example in
* <a href="http://xmpp.org/rfcs/rfc3921.html#roster-login"
* >RFC3921: Retrieving One's Roster on Login</a>.
*
* @param romeo the roster entry which should be verified.
*/
public static void verifyRomeosEntry(final RosterEntry romeo) {
assertNotNull("Can't get Romeo's roster entry!", romeo);
assertSame("Setup wrong name for Romeo!",
"Romeo",
romeo.getName());
assertSame("Setup wrong subscription status for Romeo!",
ItemType.both,
romeo.getType());
assertSame("Romeo should be member of exactly one group!",
1,
romeo.getGroups().size());
assertSame("Setup wrong group name for Romeo!",
"Friends",
romeo.getGroups().iterator().next().getName());
}
/**
* Check Mercutio's roster entry according to the example in
* <a href="http://xmpp.org/rfcs/rfc3921.html#roster-login"
* >RFC3921: Retrieving One's Roster on Login</a>.
*
* @param mercutio the roster entry which should be verified.
*/
public static void verifyMercutiosEntry(final RosterEntry mercutio) {
assertNotNull("Can't get Mercutio's roster entry!", mercutio);
assertSame("Setup wrong name for Mercutio!",
"Mercutio",
mercutio.getName());
assertSame("Setup wrong subscription status for Mercutio!",
ItemType.from,
mercutio.getType());
assertTrue("Mercutio shouldn't be a member of any group!",
mercutio.getGroups().isEmpty());
}
/**
* Check Benvolio's roster entry according to the example in
* <a href="http://xmpp.org/rfcs/rfc3921.html#roster-login"
* >RFC3921: Retrieving One's Roster on Login</a>.
*
* @param benvolio the roster entry which should be verified.
*/
public static void verifyBenvoliosEntry(final RosterEntry benvolio) {
assertNotNull("Can't get Benvolio's roster entry!", benvolio);
assertSame("Setup wrong name for Benvolio!",
"Benvolio",
benvolio.getName());
assertSame("Setup wrong subscription status for Benvolio!",
ItemType.both,
benvolio.getType());
assertTrue("Benvolio shouldn't be a member of any group!",
benvolio.getGroups().isEmpty());
}
/**
* This class can be used to simulate the server response for
* a roster update request.
*/
private abstract class RosterUpdateResponder extends Thread {
private Throwable exception = null;
/**
* Overwrite this method to check if the received update request is valid.
*
* @param updateRequest the request which would be sent to the server.
*/
abstract void verifyUpdateRequest(final RosterPacket updateRequest);
public void run() {
try {
while (true) {
final Packet packet = connection.getSentPacket();
if (packet instanceof RosterPacket && ((IQ) packet).getType() == Type.set) {
final RosterPacket rosterRequest = (RosterPacket) packet;
// Prepare and process the roster push
final RosterPacket rosterPush = new RosterPacket();
final Item item = rosterRequest.getRosterItems().iterator().next();
if (item.getItemType() != ItemType.remove) {
item.setItemType(ItemType.none);
}
rosterPush.setType(Type.set);
rosterPush.setTo(connection.getUser());
rosterPush.addRosterItem(item);
connection.processPacket(rosterPush);
// Create and process the IQ response
final IQ response = IQ.createResultIQ(rosterRequest);
connection.processPacket(response);
// Verify the roster update request
assertSame("A roster set MUST contain one and only one <item/> element.",
1,
rosterRequest.getRosterItemCount());
verifyUpdateRequest(rosterRequest);
break;
}
}
}
catch (Throwable e) {
exception = e;
fail(e.getMessage());
}
}
/**
* Returns the exception or error if something went wrong.
*
* @return the Throwable exception or error that occurred.
*/
public Throwable getException() {
return exception;
}
}
/**
* This class can be used to check if the RosterListener was invoked.
*/
public static class TestRosterListener extends WaitForPacketListener implements RosterListener {
private final List<String> addressesAdded = new CopyOnWriteArrayList<>();
private final List<String> addressesDeleted = new CopyOnWriteArrayList<>();
private final List<String> addressesUpdated = new CopyOnWriteArrayList<>();
public synchronized void entriesAdded(Collection<String> addresses) {
addressesAdded.addAll(addresses);
if (SmackConfiguration.DEBUG) {
for (String address : addresses) {
System.out.println("Roster entry for " + address + " added.");
}
}
reportInvoked();
}
public synchronized void entriesDeleted(Collection<String> addresses) {
addressesDeleted.addAll(addresses);
if (SmackConfiguration.DEBUG) {
for (String address : addresses) {
System.out.println("Roster entry for " + address + " deleted.");
}
}
reportInvoked();
}
public synchronized void entriesUpdated(Collection<String> addresses) {
addressesUpdated.addAll(addresses);
if (SmackConfiguration.DEBUG) {
for (String address : addresses) {
System.out.println("Roster entry for " + address + " updated.");
}
}
reportInvoked();
}
public void presenceChanged(Presence presence) {
if (SmackConfiguration.DEBUG) {
System.out.println("Roster presence changed: " + presence.toXML());
}
reportInvoked();
}
/**
* Get a collection of JIDs of the added roster items.
*
* @return the collection of addresses which were added.
*/
public Collection<String> getAddedAddresses() {
return Collections.unmodifiableCollection(addressesAdded);
}
/**
* Get a collection of JIDs of the deleted roster items.
*
* @return the collection of addresses which were deleted.
*/
public Collection<String> getDeletedAddresses() {
return Collections.unmodifiableCollection(addressesDeleted);
}
/**
* Get a collection of JIDs of the updated roster items.
*
* @return the collection of addresses which were updated.
*/
public Collection<String> getUpdatedAddresses() {
return Collections.unmodifiableCollection(addressesUpdated);
}
/**
* Reset the lists of added, deleted or updated items.
*/
public synchronized void reset() {
super.reset();
addressesAdded.clear();
addressesDeleted.clear();
addressesUpdated.clear();
}
}
}

View file

@ -0,0 +1,261 @@
/**
*
* Copyright the original author or authors
*
* 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.roster;
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.DummyConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.ConnectionConfiguration.Builder;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.IQ.Type;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.roster.RosterTest.TestRosterListener;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jivesoftware.smack.roster.packet.RosterPacket.Item;
import org.jivesoftware.smack.roster.packet.RosterPacket.ItemType;
import org.jivesoftware.smack.roster.rosterstore.DirectoryRosterStore;
import org.jivesoftware.smack.roster.rosterstore.RosterStore;
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 {@link 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;
private Roster roster;
private TestRosterListener rosterListener;
@Rule
public TemporaryFolder tmpFolder = new TemporaryFolder();
@Before
public void setUp() throws Exception {
DirectoryRosterStore store = DirectoryRosterStore.init(tmpFolder.newFolder("store"));
populateStore(store);
Builder<?, ?> builder = DummyConnection.getDummyConfigurationBuilder();
connection = new DummyConnection(builder.build());
connection.connect();
connection.login();
rosterListener = new TestRosterListener();
roster = Roster.getInstanceFor(connection);
roster.setRosterStore(store);
roster.addRosterListener(rosterListener);
roster.reload();
}
@After
public void tearDown() throws Exception {
if (connection != null) {
if (rosterListener != null && roster != null) {
roster.removeRosterListener(rosterListener);
rosterListener = 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.
* @throws SmackException
* @throws XMPPException
*/
@Test(timeout = 5000)
public void testEqualVersionStored() throws InterruptedException, IOException, XMPPException, SmackException {
answerWithEmptyRosterResult();
roster.waitUntilLoaded();
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 = DirectoryRosterStore.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.
* @throws SmackException
* @throws XMPPException
*/
@Test(timeout = 5000)
public void testOtherVersionStored() throws InterruptedException, XMPPException, SmackException {
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);
rosterListener.reset();
connection.processPacket(answer);
rosterListener.waitUntilInvocationOrTimeout();
} else {
assertTrue("Expected to get a RosterPacket ", false);
}
Roster roster = Roster.getInstanceFor(connection);
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 = roster.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 {
answerWithEmptyRosterResult();
rosterListener.waitAndReset();
RosterStore store = roster.getRosterStore();
// 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);
rosterListener.reset();
connection.processPacket(rosterPush);
rosterListener.waitAndReset();
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);
rosterListener.reset();
connection.processPacket(rosterPush);
rosterListener.waitAndReset();
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);
}
}
}

View file

@ -0,0 +1,224 @@
/**
*
* Copyright the original author or authors
*
* 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.roster.rosterstore;
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.roster.packet.RosterPacket;
import org.jivesoftware.smack.roster.packet.RosterPacket.Item;
import org.jivesoftware.smack.roster.packet.RosterPacket.ItemStatus;
import org.jivesoftware.smack.roster.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 DirectoryRosterStore}.
*
* @author Lars Noschinski
*/
public class DirectoryRosterStoreTest {
@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(DirectoryRosterStore.open(storeDir));
}
/**
* Tests that an initialized directory is empty.
*/
@Test
public void testStoreInitializedEmpty() throws IOException {
File storeDir = tmpFolder.newFolder();
DirectoryRosterStore store = DirectoryRosterStore.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();
DirectoryRosterStore store = DirectoryRosterStore.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();
DirectoryRosterStore store = DirectoryRosterStore.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());
}
}