1
0
Fork 0
mirror of https://codeberg.org/Mercury-IM/Smack synced 2024-06-25 04:54:50 +02:00
Smack/extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java
Florian Schmaus 1e57f1c659 Activate checkstyle and add missing license headers
Delete also all "All rights reserved" statements, as they are
unnecessary and conflict with checkstyle's header check. Delete unused
imports.
2014-02-17 20:09:55 +01:00

2747 lines
121 KiB
Java

/**
*
* 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.smackx.muc;
import java.lang.ref.WeakReference;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ConnectionCreationListener;
import org.jivesoftware.smack.ConnectionListener;
import org.jivesoftware.smack.MessageListener;
import org.jivesoftware.smack.PacketCollector;
import org.jivesoftware.smack.PacketInterceptor;
import org.jivesoftware.smack.PacketListener;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.FromMatchesFilter;
import org.jivesoftware.smack.filter.MessageTypeFilter;
import org.jivesoftware.smack.filter.PacketExtensionFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.PacketIDFilter;
import org.jivesoftware.smack.filter.PacketTypeFilter;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.packet.PacketExtension;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Registration;
import org.jivesoftware.smackx.disco.NodeInformationProvider;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.disco.packet.DiscoverItems;
import org.jivesoftware.smackx.muc.packet.MUCAdmin;
import org.jivesoftware.smackx.muc.packet.MUCInitialPresence;
import org.jivesoftware.smackx.muc.packet.MUCOwner;
import org.jivesoftware.smackx.muc.packet.MUCUser;
import org.jivesoftware.smackx.xdata.Form;
/**
* A MultiUserChat is a conversation that takes place among many users in a virtual
* room. A room could have many occupants with different affiliation and roles.
* Possible affiliatons are "owner", "admin", "member", and "outcast". Possible roles
* are "moderator", "participant", and "visitor". Each role and affiliation guarantees
* different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
* Grant voice, Edit member list, etc.).
*
* @author Gaston Dombiak, Larry Kirschner
*/
public class MultiUserChat {
private static Logger log = Logger.getLogger(MultiUserChat.class.getName());
private final static String discoNamespace = "http://jabber.org/protocol/muc";
private final static String discoNode = "http://jabber.org/protocol/muc#rooms";
private static Map<Connection, List<String>> joinedRooms =
new WeakHashMap<Connection, List<String>>();
private Connection connection;
private String room;
private String subject;
private String nickname = null;
private boolean joined = false;
private Map<String, Presence> occupantsMap = new ConcurrentHashMap<String, Presence>();
private final List<InvitationRejectionListener> invitationRejectionListeners =
new ArrayList<InvitationRejectionListener>();
private final List<SubjectUpdatedListener> subjectUpdatedListeners =
new ArrayList<SubjectUpdatedListener>();
private final List<UserStatusListener> userStatusListeners =
new ArrayList<UserStatusListener>();
private final List<ParticipantStatusListener> participantStatusListeners =
new ArrayList<ParticipantStatusListener>();
private PacketFilter presenceFilter;
private List<PacketInterceptor> presenceInterceptors = new ArrayList<PacketInterceptor>();
private PacketFilter messageFilter;
private RoomListenerMultiplexor roomListenerMultiplexor;
private ConnectionDetachedPacketCollector messageCollector;
private List<PacketListener> connectionListeners = new ArrayList<PacketListener>();
static {
Connection.addConnectionCreationListener(new ConnectionCreationListener() {
public void connectionCreated(final Connection connection) {
// Set on every established connection that this client supports the Multi-User
// Chat protocol. This information will be used when another client tries to
// discover whether this client supports MUC or not.
ServiceDiscoveryManager.getInstanceFor(connection).addFeature(discoNamespace);
// Set the NodeInformationProvider that will provide information about the
// joined rooms whenever a disco request is received
final WeakReference<Connection> weakRefConnection = new WeakReference<Connection>(connection);
ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(
discoNode,
new NodeInformationProvider() {
public List<DiscoverItems.Item> getNodeItems() {
Connection connection = weakRefConnection.get();
if (connection == null) return new LinkedList<DiscoverItems.Item>();
List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
Iterator<String> rooms=MultiUserChat.getJoinedRooms(connection);
while (rooms.hasNext()) {
answer.add(new DiscoverItems.Item(rooms.next()));
}
return answer;
}
public List<String> getNodeFeatures() {
return null;
}
public List<DiscoverInfo.Identity> getNodeIdentities() {
return null;
}
@Override
public List<PacketExtension> getNodePacketExtensions() {
return null;
}
});
}
});
}
/**
* Creates a new multi user chat with the specified connection and room name. Note: no
* information is sent to or received from the server until you attempt to
* {@link #join(String) join} the chat room. On some server implementations,
* the room will not be created until the first person joins it.<p>
*
* Most XMPP servers use a sub-domain for the chat service (eg chat.example.com
* for the XMPP server example.com). You must ensure that the room address you're
* trying to connect to includes the proper chat sub-domain.
*
* @param connection the XMPP connection.
* @param room the name of the room in the form "roomName@service", where
* "service" is the hostname at which the multi-user chat
* service is running. Make sure to provide a valid JID.
*/
public MultiUserChat(Connection connection, String room) {
this.connection = connection;
this.room = room.toLowerCase();
init();
}
/**
* Returns true if the specified user supports the Multi-User Chat protocol.
*
* @param connection the connection to use to perform the service discovery.
* @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
* @return a boolean indicating whether the specified user supports the MUC protocol.
*/
public static boolean isServiceEnabled(Connection connection, String user) {
try {
DiscoverInfo result =
ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(user);
return result.containsFeature(discoNamespace);
}
catch (XMPPException e) {
log.log(Level.SEVERE, "Error checking user [" + user + "] for MUC support", e);
return false;
}
}
/**
* Returns an Iterator on the rooms where the user has joined using a given connection.
* The Iterator will contain Strings where each String represents a room
* (e.g. room@muc.jabber.org).
*
* @param connection the connection used to join the rooms.
* @return an Iterator on the rooms where the user has joined using a given connection.
*/
private static Iterator<String> getJoinedRooms(Connection connection) {
List<String> rooms = joinedRooms.get(connection);
if (rooms != null) {
return rooms.iterator();
}
// Return an iterator on an empty collection (i.e. the user never joined a room)
return new ArrayList<String>().iterator();
}
/**
* Returns an Iterator on the rooms where the requested user has joined. The Iterator will
* contain Strings where each String represents a room (e.g. room@muc.jabber.org).
*
* @param connection the connection to use to perform the service discovery.
* @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
* @return an Iterator on the rooms where the requested user has joined.
*/
public static Iterator<String> getJoinedRooms(Connection connection, String user) {
try {
ArrayList<String> answer = new ArrayList<String>();
// Send the disco packet to the user
DiscoverItems result =
ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(user, discoNode);
// Collect the entityID for each returned item
for (Iterator<DiscoverItems.Item> items=result.getItems(); items.hasNext();) {
answer.add(items.next().getEntityID());
}
return answer.iterator();
}
catch (XMPPException e) {
log.log(Level.SEVERE, "Error getting joined rooms for user [" + user + "]", e);
// Return an iterator on an empty collection
return new ArrayList<String>().iterator();
}
}
/**
* Returns the discovered information of a given room without actually having to join the room.
* The server will provide information only for rooms that are public.
*
* @param connection the XMPP connection to use for discovering information about the room.
* @param room the name of the room in the form "roomName@service" of which we want to discover
* its information.
* @return the discovered information of a given room without actually having to join the room.
* @throws XMPPException if an error occured while trying to discover information of a room.
*/
public static RoomInfo getRoomInfo(Connection connection, String room)
throws XMPPException {
DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(room);
return new RoomInfo(info);
}
/**
* Returns a collection with the XMPP addresses of the Multi-User Chat services.
*
* @param connection the XMPP connection to use for discovering Multi-User Chat services.
* @return a collection with the XMPP addresses of the Multi-User Chat services.
* @throws XMPPException if an error occured while trying to discover MUC services.
*/
public static Collection<String> getServiceNames(Connection connection) throws XMPPException {
final List<String> answer = new ArrayList<String>();
ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
DiscoverItems items = discoManager.discoverItems(connection.getServiceName());
for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
DiscoverItems.Item item = it.next();
try {
DiscoverInfo info = discoManager.discoverInfo(item.getEntityID());
if (info.containsFeature("http://jabber.org/protocol/muc")) {
answer.add(item.getEntityID());
}
}
catch (XMPPException e) {
// Trouble finding info in some cases. This is a workaround for
// discovering info on remote servers.
}
}
return answer;
}
/**
* Returns a collection of HostedRooms where each HostedRoom has the XMPP address of the room
* and the room's name. Once discovered the rooms hosted by a chat service it is possible to
* discover more detailed room information or join the room.
*
* @param connection the XMPP connection to use for discovering hosted rooms by the MUC service.
* @param serviceName the service that is hosting the rooms to discover.
* @return a collection of HostedRooms.
* @throws XMPPException if an error occured while trying to discover the information.
*/
public static Collection<HostedRoom> getHostedRooms(Connection connection, String serviceName)
throws XMPPException {
List<HostedRoom> answer = new ArrayList<HostedRoom>();
ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
DiscoverItems items = discoManager.discoverItems(serviceName);
for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
answer.add(new HostedRoom(it.next()));
}
return answer;
}
/**
* Returns the name of the room this MultiUserChat object represents.
*
* @return the multi user chat room name.
*/
public String getRoom() {
return room;
}
/**
* Creates the room according to some default configuration, assign the requesting user
* as the room owner, and add the owner to the room but not allow anyone else to enter
* the room (effectively "locking" the room). The requesting user will join the room
* under the specified nickname as soon as the room has been created.<p>
*
* To create an "Instant Room", that means a room with some default configuration that is
* available for immediate access, the room's owner should send an empty form after creating
* the room. {@link #sendConfigurationForm(Form)}<p>
*
* To create a "Reserved Room", that means a room manually configured by the room creator
* before anyone is allowed to enter, the room's owner should complete and send a form after
* creating the room. Once the completed configutation form is sent to the server, the server
* will unlock the room. {@link #sendConfigurationForm(Form)}
*
* @param nickname the nickname to use.
* @throws XMPPException if the room couldn't be created for some reason
* (e.g. room already exists; user already joined to an existant room or
* 405 error if the user is not allowed to create the room)
*/
public synchronized void create(String nickname) throws XMPPException {
if (nickname == null || nickname.equals("")) {
throw new IllegalArgumentException("Nickname must not be null or blank.");
}
// If we've already joined the room, leave it before joining under a new
// nickname.
if (joined) {
throw new IllegalStateException("Creation failed - User already joined the room.");
}
// We create a room by sending a presence packet to room@service/nick
// and signal support for MUC. The owner will be automatically logged into the room.
Presence joinPresence = new Presence(Presence.Type.available);
joinPresence.setTo(room + "/" + nickname);
// Indicate the the client supports MUC
joinPresence.addExtension(new MUCInitialPresence());
// Invoke presence interceptors so that extra information can be dynamically added
for (PacketInterceptor packetInterceptor : presenceInterceptors) {
packetInterceptor.interceptPacket(joinPresence);
}
// Wait for a presence packet back from the server.
PacketFilter responseFilter =
new AndFilter(
new FromMatchesFilter(room + "/" + nickname),
new PacketTypeFilter(Presence.class));
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send create & join packet.
connection.sendPacket(joinPresence);
// Wait up to a certain number of seconds for a reply.
Presence presence =
(Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (presence == null) {
throw new XMPPException("No response from server.");
}
else if (presence.getError() != null) {
throw new XMPPException(presence.getError());
}
// Whether the room existed before or was created, the user has joined the room
this.nickname = nickname;
joined = true;
userHasJoined();
// Look for confirmation of room creation from the server
MUCUser mucUser = getMUCUserExtension(presence);
if (mucUser != null && mucUser.getStatus() != null) {
if ("201".equals(mucUser.getStatus().getCode())) {
// Room was created and the user has joined the room
return;
}
}
// We need to leave the room since it seems that the room already existed
leave();
throw new XMPPException("Creation failed - Missing acknowledge of room creation.");
}
/**
* Joins the chat room using the specified nickname. If already joined
* using another nickname, this method will first leave the room and then
* re-join using the new nickname. The default timeout of Smack for a reply
* from the group chat server that the join succeeded will be used. After
* joining the room, the room will decide the amount of history to send.
*
* @param nickname the nickname to use.
* @throws XMPPException if an error occurs joining the room. In particular, a
* 401 error can occur if no password was provided and one is required; or a
* 403 error can occur if the user is banned; or a
* 404 error can occur if the room does not exist or is locked; or a
* 407 error can occur if user is not on the member list; or a
* 409 error can occur if someone is already in the group chat with the same nickname.
*/
public void join(String nickname) throws XMPPException {
join(nickname, null, null, SmackConfiguration.getPacketReplyTimeout());
}
/**
* Joins the chat room using the specified nickname and password. If already joined
* using another nickname, this method will first leave the room and then
* re-join using the new nickname. The default timeout of Smack for a reply
* from the group chat server that the join succeeded will be used. After
* joining the room, the room will decide the amount of history to send.<p>
*
* A password is required when joining password protected rooms. If the room does
* not require a password there is no need to provide one.
*
* @param nickname the nickname to use.
* @param password the password to use.
* @throws XMPPException if an error occurs joining the room. In particular, a
* 401 error can occur if no password was provided and one is required; or a
* 403 error can occur if the user is banned; or a
* 404 error can occur if the room does not exist or is locked; or a
* 407 error can occur if user is not on the member list; or a
* 409 error can occur if someone is already in the group chat with the same nickname.
*/
public void join(String nickname, String password) throws XMPPException {
join(nickname, password, null, SmackConfiguration.getPacketReplyTimeout());
}
/**
* Joins the chat room using the specified nickname and password. If already joined
* using another nickname, this method will first leave the room and then
* re-join using the new nickname.<p>
*
* To control the amount of history to receive while joining a room you will need to provide
* a configured DiscussionHistory object.<p>
*
* A password is required when joining password protected rooms. If the room does
* not require a password there is no need to provide one.<p>
*
* If the room does not already exist when the user seeks to enter it, the server will
* decide to create a new room or not.
*
* @param nickname the nickname to use.
* @param password the password to use.
* @param history the amount of discussion history to receive while joining a room.
* @param timeout the amount of time to wait for a reply from the MUC service(in milleseconds).
* @throws XMPPException if an error occurs joining the room. In particular, a
* 401 error can occur if no password was provided and one is required; or a
* 403 error can occur if the user is banned; or a
* 404 error can occur if the room does not exist or is locked; or a
* 407 error can occur if user is not on the member list; or a
* 409 error can occur if someone is already in the group chat with the same nickname.
*/
public synchronized void join(
String nickname,
String password,
DiscussionHistory history,
long timeout)
throws XMPPException {
if (nickname == null || nickname.equals("")) {
throw new IllegalArgumentException("Nickname must not be null or blank.");
}
// If we've already joined the room, leave it before joining under a new
// nickname.
if (joined) {
leave();
}
// We join a room by sending a presence packet where the "to"
// field is in the form "roomName@service/nickname"
Presence joinPresence = new Presence(Presence.Type.available);
joinPresence.setTo(room + "/" + nickname);
// Indicate the the client supports MUC
MUCInitialPresence mucInitialPresence = new MUCInitialPresence();
if (password != null) {
mucInitialPresence.setPassword(password);
}
if (history != null) {
mucInitialPresence.setHistory(history.getMUCHistory());
}
joinPresence.addExtension(mucInitialPresence);
// Invoke presence interceptors so that extra information can be dynamically added
for (PacketInterceptor packetInterceptor : presenceInterceptors) {
packetInterceptor.interceptPacket(joinPresence);
}
// Wait for a presence packet back from the server.
PacketFilter responseFilter =
new AndFilter(
new FromMatchesFilter(room + "/" + nickname),
new PacketTypeFilter(Presence.class));
PacketCollector response = null;
Presence presence;
try {
response = connection.createPacketCollector(responseFilter);
// Send join packet.
connection.sendPacket(joinPresence);
// Wait up to a certain number of seconds for a reply.
presence = (Presence) response.nextResult(timeout);
}
finally {
// Stop queuing results
if (response != null) {
response.cancel();
}
}
if (presence == null) {
throw new XMPPException("No response from server.");
}
else if (presence.getError() != null) {
throw new XMPPException(presence.getError());
}
this.nickname = nickname;
joined = true;
userHasJoined();
}
/**
* Returns true if currently in the multi user chat (after calling the {@link
* #join(String)} method).
*
* @return true if currently in the multi user chat room.
*/
public boolean isJoined() {
return joined;
}
/**
* Leave the chat room.
*/
public synchronized void leave() {
// If not joined already, do nothing.
if (!joined) {
return;
}
// We leave a room by sending a presence packet where the "to"
// field is in the form "roomName@service/nickname"
Presence leavePresence = new Presence(Presence.Type.unavailable);
leavePresence.setTo(room + "/" + nickname);
// Invoke presence interceptors so that extra information can be dynamically added
for (PacketInterceptor packetInterceptor : presenceInterceptors) {
packetInterceptor.interceptPacket(leavePresence);
}
connection.sendPacket(leavePresence);
// Reset occupant information.
occupantsMap.clear();
nickname = null;
joined = false;
userHasLeft();
}
/**
* Returns the room's configuration form that the room's owner can use or <tt>null</tt> if
* no configuration is possible. The configuration form allows to set the room's language,
* enable logging, specify room's type, etc..
*
* @return the Form that contains the fields to complete together with the instrucions or
* <tt>null</tt> if no configuration is possible.
* @throws XMPPException if an error occurs asking the configuration form for the room.
*/
public Form getConfigurationForm() throws XMPPException {
MUCOwner iq = new MUCOwner();
iq.setTo(room);
iq.setType(IQ.Type.GET);
// Filter packets looking for an answer from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Request the configuration form to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
return Form.getFormFrom(answer);
}
/**
* Sends the completed configuration form to the server. The room will be configured
* with the new settings defined in the form. If the form is empty then the server
* will create an instant room (will use default configuration).
*
* @param form the form with the new settings.
* @throws XMPPException if an error occurs setting the new rooms' configuration.
*/
public void sendConfigurationForm(Form form) throws XMPPException {
MUCOwner iq = new MUCOwner();
iq.setTo(room);
iq.setType(IQ.Type.SET);
iq.addExtension(form.getDataFormToSend());
// Filter packets looking for an answer from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the completed configuration form to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
}
/**
* Returns the room's registration form that an unaffiliated user, can use to become a member
* of the room or <tt>null</tt> if no registration is possible. Some rooms may restrict the
* privilege to register members and allow only room admins to add new members.<p>
*
* If the user requesting registration requirements is not allowed to register with the room
* (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
* error to the user (error code 405).
*
* @return the registration Form that contains the fields to complete together with the
* instrucions or <tt>null</tt> if no registration is possible.
* @throws XMPPException if an error occurs asking the registration form for the room or a
* 405 error if the user is not allowed to register with the room.
*/
public Form getRegistrationForm() throws XMPPException {
Registration reg = new Registration();
reg.setType(IQ.Type.GET);
reg.setTo(room);
PacketFilter filter =
new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
PacketCollector collector = connection.createPacketCollector(filter);
connection.sendPacket(reg);
IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
collector.cancel();
if (result == null) {
throw new XMPPException("No response from server.");
}
else if (result.getType() == IQ.Type.ERROR) {
throw new XMPPException(result.getError());
}
return Form.getFormFrom(result);
}
/**
* Sends the completed registration form to the server. After the user successfully submits
* the form, the room may queue the request for review by the room admins or may immediately
* add the user to the member list by changing the user's affiliation from "none" to "member.<p>
*
* If the desired room nickname is already reserved for that room, the room will return a
* "Conflict" error to the user (error code 409). If the room does not support registration,
* it will return a "Service Unavailable" error to the user (error code 503).
*
* @param form the completed registration form.
* @throws XMPPException if an error occurs submitting the registration form. In particular, a
* 409 error can occur if the desired room nickname is already reserved for that room;
* or a 503 error can occur if the room does not support registration.
*/
public void sendRegistrationForm(Form form) throws XMPPException {
Registration reg = new Registration();
reg.setType(IQ.Type.SET);
reg.setTo(room);
reg.addExtension(form.getDataFormToSend());
PacketFilter filter =
new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
PacketCollector collector = connection.createPacketCollector(filter);
connection.sendPacket(reg);
IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
collector.cancel();
if (result == null) {
throw new XMPPException("No response from server.");
}
else if (result.getType() == IQ.Type.ERROR) {
throw new XMPPException(result.getError());
}
}
/**
* Sends a request to the server to destroy the room. The sender of the request
* should be the room's owner. If the sender of the destroy request is not the room's owner
* then the server will answer a "Forbidden" error (403).
*
* @param reason the reason for the room destruction.
* @param alternateJID the JID of an alternate location.
* @throws XMPPException if an error occurs while trying to destroy the room.
* An error can occur which will be wrapped by an XMPPException --
* XMPP error code 403. The error code can be used to present more
* appropiate error messages to end-users.
*/
public void destroy(String reason, String alternateJID) throws XMPPException {
MUCOwner iq = new MUCOwner();
iq.setTo(room);
iq.setType(IQ.Type.SET);
// Create the reason for the room destruction
MUCOwner.Destroy destroy = new MUCOwner.Destroy();
destroy.setReason(reason);
destroy.setJid(alternateJID);
iq.setDestroy(destroy);
// Wait for a presence packet back from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the room destruction request.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
// Reset occupant information.
occupantsMap.clear();
nickname = null;
joined = false;
userHasLeft();
}
/**
* Invites another user to the room in which one is an occupant. The invitation
* will be sent to the room which in turn will forward the invitation to the invitee.<p>
*
* If the room is password-protected, the invitee will receive a password to use to join
* the room. If the room is members-only, the the invitee may be added to the member list.
*
* @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
* @param reason the reason why the user is being invited.
*/
public void invite(String user, String reason) {
invite(new Message(), user, reason);
}
/**
* Invites another user to the room in which one is an occupant using a given Message. The invitation
* will be sent to the room which in turn will forward the invitation to the invitee.<p>
*
* If the room is password-protected, the invitee will receive a password to use to join
* the room. If the room is members-only, the the invitee may be added to the member list.
*
* @param message the message to use for sending the invitation.
* @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
* @param reason the reason why the user is being invited.
*/
public void invite(Message message, String user, String reason) {
// TODO listen for 404 error code when inviter supplies a non-existent JID
message.setTo(room);
// Create the MUCUser packet that will include the invitation
MUCUser mucUser = new MUCUser();
MUCUser.Invite invite = new MUCUser.Invite();
invite.setTo(user);
invite.setReason(reason);
mucUser.setInvite(invite);
// Add the MUCUser packet that includes the invitation to the message
message.addExtension(mucUser);
connection.sendPacket(message);
}
/**
* Informs the sender of an invitation that the invitee declines the invitation. The rejection
* will be sent to the room which in turn will forward the rejection to the inviter.
*
* @param conn the connection to use for sending the rejection.
* @param room the room that sent the original invitation.
* @param inviter the inviter of the declined invitation.
* @param reason the reason why the invitee is declining the invitation.
*/
public static void decline(Connection conn, String room, String inviter, String reason) {
Message message = new Message(room);
// Create the MUCUser packet that will include the rejection
MUCUser mucUser = new MUCUser();
MUCUser.Decline decline = new MUCUser.Decline();
decline.setTo(inviter);
decline.setReason(reason);
mucUser.setDecline(decline);
// Add the MUCUser packet that includes the rejection
message.addExtension(mucUser);
conn.sendPacket(message);
}
/**
* Adds a listener to invitation notifications. The listener will be fired anytime
* an invitation is received.
*
* @param conn the connection where the listener will be applied.
* @param listener an invitation listener.
*/
public static void addInvitationListener(Connection conn, InvitationListener listener) {
InvitationsMonitor.getInvitationsMonitor(conn).addInvitationListener(listener);
}
/**
* Removes a listener to invitation notifications. The listener will be fired anytime
* an invitation is received.
*
* @param conn the connection where the listener was applied.
* @param listener an invitation listener.
*/
public static void removeInvitationListener(Connection conn, InvitationListener listener) {
InvitationsMonitor.getInvitationsMonitor(conn).removeInvitationListener(listener);
}
/**
* Adds a listener to invitation rejections notifications. The listener will be fired anytime
* an invitation is declined.
*
* @param listener an invitation rejection listener.
*/
public void addInvitationRejectionListener(InvitationRejectionListener listener) {
synchronized (invitationRejectionListeners) {
if (!invitationRejectionListeners.contains(listener)) {
invitationRejectionListeners.add(listener);
}
}
}
/**
* Removes a listener from invitation rejections notifications. The listener will be fired
* anytime an invitation is declined.
*
* @param listener an invitation rejection listener.
*/
public void removeInvitationRejectionListener(InvitationRejectionListener listener) {
synchronized (invitationRejectionListeners) {
invitationRejectionListeners.remove(listener);
}
}
/**
* Fires invitation rejection listeners.
*
* @param invitee the user being invited.
* @param reason the reason for the rejection
*/
private void fireInvitationRejectionListeners(String invitee, String reason) {
InvitationRejectionListener[] listeners;
synchronized (invitationRejectionListeners) {
listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
invitationRejectionListeners.toArray(listeners);
}
for (InvitationRejectionListener listener : listeners) {
listener.invitationDeclined(invitee, reason);
}
}
/**
* Adds a listener to subject change notifications. The listener will be fired anytime
* the room's subject changes.
*
* @param listener a subject updated listener.
*/
public void addSubjectUpdatedListener(SubjectUpdatedListener listener) {
synchronized (subjectUpdatedListeners) {
if (!subjectUpdatedListeners.contains(listener)) {
subjectUpdatedListeners.add(listener);
}
}
}
/**
* Removes a listener from subject change notifications. The listener will be fired
* anytime the room's subject changes.
*
* @param listener a subject updated listener.
*/
public void removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
synchronized (subjectUpdatedListeners) {
subjectUpdatedListeners.remove(listener);
}
}
/**
* Fires subject updated listeners.
*/
private void fireSubjectUpdatedListeners(String subject, String from) {
SubjectUpdatedListener[] listeners;
synchronized (subjectUpdatedListeners) {
listeners = new SubjectUpdatedListener[subjectUpdatedListeners.size()];
subjectUpdatedListeners.toArray(listeners);
}
for (SubjectUpdatedListener listener : listeners) {
listener.subjectUpdated(subject, from);
}
}
/**
* Adds a new {@link PacketInterceptor} that will be invoked every time a new presence
* is going to be sent by this MultiUserChat to the server. Packet interceptors may
* add new extensions to the presence that is going to be sent to the MUC service.
*
* @param presenceInterceptor the new packet interceptor that will intercept presence packets.
*/
public void addPresenceInterceptor(PacketInterceptor presenceInterceptor) {
presenceInterceptors.add(presenceInterceptor);
}
/**
* Removes a {@link PacketInterceptor} that was being invoked every time a new presence
* was being sent by this MultiUserChat to the server. Packet interceptors may
* add new extensions to the presence that is going to be sent to the MUC service.
*
* @param presenceInterceptor the packet interceptor to remove.
*/
public void removePresenceInterceptor(PacketInterceptor presenceInterceptor) {
presenceInterceptors.remove(presenceInterceptor);
}
/**
* Returns the last known room's subject or <tt>null</tt> if the user hasn't joined the room
* or the room does not have a subject yet. In case the room has a subject, as soon as the
* user joins the room a message with the current room's subject will be received.<p>
*
* To be notified every time the room's subject change you should add a listener
* to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
*
* To change the room's subject use {@link #changeSubject(String)}.
*
* @return the room's subject or <tt>null</tt> if the user hasn't joined the room or the
* room does not have a subject yet.
*/
public String getSubject() {
return subject;
}
/**
* Returns the reserved room nickname for the user in the room. A user may have a reserved
* nickname, for example through explicit room registration or database integration. In such
* cases it may be desirable for the user to discover the reserved nickname before attempting
* to enter the room.
*
* @return the reserved room nickname or <tt>null</tt> if none.
*/
public String getReservedNickname() {
try {
DiscoverInfo result =
ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
room,
"x-roomuser-item");
// Look for an Identity that holds the reserved nickname and return its name
for (Iterator<DiscoverInfo.Identity> identities = result.getIdentities();
identities.hasNext();) {
DiscoverInfo.Identity identity = identities.next();
return identity.getName();
}
}
catch (XMPPException e) {
log.log(Level.SEVERE, "Error retrieving room nickname", e);
}
// If no Identity was found then the user does not have a reserved room nickname
return null;
}
/**
* Returns the nickname that was used to join the room, or <tt>null</tt> if not
* currently joined.
*
* @return the nickname currently being used.
*/
public String getNickname() {
return nickname;
}
/**
* Changes the occupant's nickname to a new nickname within the room. Each room occupant
* will receive two presence packets. One of type "unavailable" for the old nickname and one
* indicating availability for the new nickname. The unavailable presence will contain the new
* nickname and an appropriate status code (namely 303) as extended presence information. The
* status code 303 indicates that the occupant is changing his/her nickname.
*
* @param nickname the new nickname within the room.
* @throws XMPPException if the new nickname is already in use by another occupant.
*/
public void changeNickname(String nickname) throws XMPPException {
if (nickname == null || nickname.equals("")) {
throw new IllegalArgumentException("Nickname must not be null or blank.");
}
// Check that we already have joined the room before attempting to change the
// nickname.
if (!joined) {
throw new IllegalStateException("Must be logged into the room to change nickname.");
}
// We change the nickname by sending a presence packet where the "to"
// field is in the form "roomName@service/nickname"
// We don't have to signal the MUC support again
Presence joinPresence = new Presence(Presence.Type.available);
joinPresence.setTo(room + "/" + nickname);
// Invoke presence interceptors so that extra information can be dynamically added
for (PacketInterceptor packetInterceptor : presenceInterceptors) {
packetInterceptor.interceptPacket(joinPresence);
}
// Wait for a presence packet back from the server.
PacketFilter responseFilter =
new AndFilter(
new FromMatchesFilter(room + "/" + nickname),
new PacketTypeFilter(Presence.class));
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send join packet.
connection.sendPacket(joinPresence);
// Wait up to a certain number of seconds for a reply.
Presence presence =
(Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (presence == null) {
throw new XMPPException("No response from server.");
}
else if (presence.getError() != null) {
throw new XMPPException(presence.getError());
}
this.nickname = nickname;
}
/**
* Changes the occupant's availability status within the room. The presence type
* will remain available but with a new status that describes the presence update and
* a new presence mode (e.g. Extended away).
*
* @param status a text message describing the presence update.
* @param mode the mode type for the presence update.
*/
public void changeAvailabilityStatus(String status, Presence.Mode mode) {
if (nickname == null || nickname.equals("")) {
throw new IllegalArgumentException("Nickname must not be null or blank.");
}
// Check that we already have joined the room before attempting to change the
// availability status.
if (!joined) {
throw new IllegalStateException(
"Must be logged into the room to change the " + "availability status.");
}
// We change the availability status by sending a presence packet to the room with the
// new presence status and mode
Presence joinPresence = new Presence(Presence.Type.available);
joinPresence.setStatus(status);
joinPresence.setMode(mode);
joinPresence.setTo(room + "/" + nickname);
// Invoke presence interceptors so that extra information can be dynamically added
for (PacketInterceptor packetInterceptor : presenceInterceptors) {
packetInterceptor.interceptPacket(joinPresence);
}
// Send join packet.
connection.sendPacket(joinPresence);
}
/**
* Kicks a visitor or participant from the room. The kicked occupant will receive a presence
* of type "unavailable" including a status code 307 and optionally along with the reason
* (if provided) and the bare JID of the user who initiated the kick. After the occupant
* was kicked from the room, the rest of the occupants will receive a presence of type
* "unavailable". The presence will include a status code 307 which means that the occupant
* was kicked from the room.
*
* @param nickname the nickname of the participant or visitor to kick from the room
* (e.g. "john").
* @param reason the reason why the participant or visitor is being kicked from the room.
* @throws XMPPException if an error occurs kicking the occupant. In particular, a
* 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
* was intended to be kicked (i.e. Not Allowed error); or a
* 403 error can occur if the occupant that intended to kick another occupant does
* not have kicking privileges (i.e. Forbidden error); or a
* 400 error can occur if the provided nickname is not present in the room.
*/
public void kickParticipant(String nickname, String reason) throws XMPPException {
changeRole(nickname, "none", reason);
}
/**
* Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
* who does and does not have "voice" in the room. To have voice means that a room occupant
* is able to send messages to the room occupants.
*
* @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
* @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
* 403 error can occur if the occupant that intended to grant voice is not
* a moderator in this room (i.e. Forbidden error); or a
* 400 error can occur if the provided nickname is not present in the room.
*/
public void grantVoice(Collection<String> nicknames) throws XMPPException {
changeRole(nicknames, "participant");
}
/**
* Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
* who does and does not have "voice" in the room. To have voice means that a room occupant
* is able to send messages to the room occupants.
*
* @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
* @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
* 403 error can occur if the occupant that intended to grant voice is not
* a moderator in this room (i.e. Forbidden error); or a
* 400 error can occur if the provided nickname is not present in the room.
*/
public void grantVoice(String nickname) throws XMPPException {
changeRole(nickname, "participant", null);
}
/**
* Revokes voice from participants in the room. In a moderated room, a moderator may want to
* revoke an occupant's privileges to speak. To have voice means that a room occupant
* is able to send messages to the room occupants.
*
* @param nicknames the nicknames of the participants to revoke voice (e.g. "john").
* @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
* 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
* was tried to revoke his voice (i.e. Not Allowed error); or a
* 400 error can occur if the provided nickname is not present in the room.
*/
public void revokeVoice(Collection<String> nicknames) throws XMPPException {
changeRole(nicknames, "visitor");
}
/**
* Revokes voice from a participant in the room. In a moderated room, a moderator may want to
* revoke an occupant's privileges to speak. To have voice means that a room occupant
* is able to send messages to the room occupants.
*
* @param nickname the nickname of the participant to revoke voice (e.g. "john").
* @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
* 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
* was tried to revoke his voice (i.e. Not Allowed error); or a
* 400 error can occur if the provided nickname is not present in the room.
*/
public void revokeVoice(String nickname) throws XMPPException {
changeRole(nickname, "visitor", null);
}
/**
* Bans users from the room. An admin or owner of the room can ban users from a room. This
* means that the banned user will no longer be able to join the room unless the ban has been
* removed. If the banned user was present in the room then he/she will be removed from the
* room and notified that he/she was banned along with the reason (if provided) and the bare
* XMPP user ID of the user who initiated the ban.
*
* @param jids the bare XMPP user IDs of the users to ban.
* @throws XMPPException if an error occurs banning a user. In particular, a
* 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
* was tried to be banned (i.e. Not Allowed error).
*/
public void banUsers(Collection<String> jids) throws XMPPException {
changeAffiliationByAdmin(jids, "outcast");
}
/**
* Bans a user from the room. An admin or owner of the room can ban users from a room. This
* means that the banned user will no longer be able to join the room unless the ban has been
* removed. If the banned user was present in the room then he/she will be removed from the
* room and notified that he/she was banned along with the reason (if provided) and the bare
* XMPP user ID of the user who initiated the ban.
*
* @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org").
* @param reason the optional reason why the user was banned.
* @throws XMPPException if an error occurs banning a user. In particular, a
* 405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
* was tried to be banned (i.e. Not Allowed error).
*/
public void banUser(String jid, String reason) throws XMPPException {
changeAffiliationByAdmin(jid, "outcast", reason);
}
/**
* Grants membership to other users. Only administrators are able to grant membership. A user
* that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
* that a user cannot enter without being on the member list).
*
* @param jids the XMPP user IDs of the users to grant membership.
* @throws XMPPException if an error occurs granting membership to a user.
*/
public void grantMembership(Collection<String> jids) throws XMPPException {
changeAffiliationByAdmin(jids, "member");
}
/**
* Grants membership to a user. Only administrators are able to grant membership. A user
* that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
* that a user cannot enter without being on the member list).
*
* @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org").
* @throws XMPPException if an error occurs granting membership to a user.
*/
public void grantMembership(String jid) throws XMPPException {
changeAffiliationByAdmin(jid, "member", null);
}
/**
* Revokes users' membership. Only administrators are able to revoke membership. A user
* that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
* that a user cannot enter without being on the member list). If the user is in the room and
* the room is of type members-only then the user will be removed from the room.
*
* @param jids the bare XMPP user IDs of the users to revoke membership.
* @throws XMPPException if an error occurs revoking membership to a user.
*/
public void revokeMembership(Collection<String> jids) throws XMPPException {
changeAffiliationByAdmin(jids, "none");
}
/**
* Revokes a user's membership. Only administrators are able to revoke membership. A user
* that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
* that a user cannot enter without being on the member list). If the user is in the room and
* the room is of type members-only then the user will be removed from the room.
*
* @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org").
* @throws XMPPException if an error occurs revoking membership to a user.
*/
public void revokeMembership(String jid) throws XMPPException {
changeAffiliationByAdmin(jid, "none", null);
}
/**
* Grants moderator privileges to participants or visitors. Room administrators may grant
* moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
* other users, modify room's subject plus all the partcipants privileges.
*
* @param nicknames the nicknames of the occupants to grant moderator privileges.
* @throws XMPPException if an error occurs granting moderator privileges to a user.
*/
public void grantModerator(Collection<String> nicknames) throws XMPPException {
changeRole(nicknames, "moderator");
}
/**
* Grants moderator privileges to a participant or visitor. Room administrators may grant
* moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
* other users, modify room's subject plus all the partcipants privileges.
*
* @param nickname the nickname of the occupant to grant moderator privileges.
* @throws XMPPException if an error occurs granting moderator privileges to a user.
*/
public void grantModerator(String nickname) throws XMPPException {
changeRole(nickname, "moderator", null);
}
/**
* Revokes moderator privileges from other users. The occupant that loses moderator
* privileges will become a participant. Room administrators may revoke moderator privileges
* only to occupants whose affiliation is member or none. This means that an administrator is
* not allowed to revoke moderator privileges from other room administrators or owners.
*
* @param nicknames the nicknames of the occupants to revoke moderator privileges.
* @throws XMPPException if an error occurs revoking moderator privileges from a user.
*/
public void revokeModerator(Collection<String> nicknames) throws XMPPException {
changeRole(nicknames, "participant");
}
/**
* Revokes moderator privileges from another user. The occupant that loses moderator
* privileges will become a participant. Room administrators may revoke moderator privileges
* only to occupants whose affiliation is member or none. This means that an administrator is
* not allowed to revoke moderator privileges from other room administrators or owners.
*
* @param nickname the nickname of the occupant to revoke moderator privileges.
* @throws XMPPException if an error occurs revoking moderator privileges from a user.
*/
public void revokeModerator(String nickname) throws XMPPException {
changeRole(nickname, "participant", null);
}
/**
* Grants ownership privileges to other users. Room owners may grant ownership privileges.
* Some room implementations will not allow to grant ownership privileges to other users.
* An owner is allowed to change defining room features as well as perform all administrative
* functions.
*
* @param jids the collection of bare XMPP user IDs of the users to grant ownership.
* @throws XMPPException if an error occurs granting ownership privileges to a user.
*/
public void grantOwnership(Collection<String> jids) throws XMPPException {
changeAffiliationByAdmin(jids, "owner");
}
/**
* Grants ownership privileges to another user. Room owners may grant ownership privileges.
* Some room implementations will not allow to grant ownership privileges to other users.
* An owner is allowed to change defining room features as well as perform all administrative
* functions.
*
* @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org").
* @throws XMPPException if an error occurs granting ownership privileges to a user.
*/
public void grantOwnership(String jid) throws XMPPException {
changeAffiliationByAdmin(jid, "owner", null);
}
/**
* Revokes ownership privileges from other users. The occupant that loses ownership
* privileges will become an administrator. Room owners may revoke ownership privileges.
* Some room implementations will not allow to grant ownership privileges to other users.
*
* @param jids the bare XMPP user IDs of the users to revoke ownership.
* @throws XMPPException if an error occurs revoking ownership privileges from a user.
*/
public void revokeOwnership(Collection<String> jids) throws XMPPException {
changeAffiliationByAdmin(jids, "admin");
}
/**
* Revokes ownership privileges from another user. The occupant that loses ownership
* privileges will become an administrator. Room owners may revoke ownership privileges.
* Some room implementations will not allow to grant ownership privileges to other users.
*
* @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org").
* @throws XMPPException if an error occurs revoking ownership privileges from a user.
*/
public void revokeOwnership(String jid) throws XMPPException {
changeAffiliationByAdmin(jid, "admin", null);
}
/**
* Grants administrator privileges to other users. Room owners may grant administrator
* privileges to a member or unaffiliated user. An administrator is allowed to perform
* administrative functions such as banning users and edit moderator list.
*
* @param jids the bare XMPP user IDs of the users to grant administrator privileges.
* @throws XMPPException if an error occurs granting administrator privileges to a user.
*/
public void grantAdmin(Collection<String> jids) throws XMPPException {
changeAffiliationByOwner(jids, "admin");
}
/**
* Grants administrator privileges to another user. Room owners may grant administrator
* privileges to a member or unaffiliated user. An administrator is allowed to perform
* administrative functions such as banning users and edit moderator list.
*
* @param jid the bare XMPP user ID of the user to grant administrator privileges
* (e.g. "user@host.org").
* @throws XMPPException if an error occurs granting administrator privileges to a user.
*/
public void grantAdmin(String jid) throws XMPPException {
changeAffiliationByOwner(jid, "admin");
}
/**
* Revokes administrator privileges from users. The occupant that loses administrator
* privileges will become a member. Room owners may revoke administrator privileges from
* a member or unaffiliated user.
*
* @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
* @throws XMPPException if an error occurs revoking administrator privileges from a user.
*/
public void revokeAdmin(Collection<String> jids) throws XMPPException {
changeAffiliationByOwner(jids, "member");
}
/**
* Revokes administrator privileges from a user. The occupant that loses administrator
* privileges will become a member. Room owners may revoke administrator privileges from
* a member or unaffiliated user.
*
* @param jid the bare XMPP user ID of the user to revoke administrator privileges
* (e.g. "user@host.org").
* @throws XMPPException if an error occurs revoking administrator privileges from a user.
*/
public void revokeAdmin(String jid) throws XMPPException {
changeAffiliationByOwner(jid, "member");
}
private void changeAffiliationByOwner(String jid, String affiliation) throws XMPPException {
MUCOwner iq = new MUCOwner();
iq.setTo(room);
iq.setType(IQ.Type.SET);
// Set the new affiliation.
MUCOwner.Item item = new MUCOwner.Item(affiliation);
item.setJid(jid);
iq.addItem(item);
// Wait for a response packet back from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the change request to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
}
private void changeAffiliationByOwner(Collection<String> jids, String affiliation)
throws XMPPException {
MUCOwner iq = new MUCOwner();
iq.setTo(room);
iq.setType(IQ.Type.SET);
for (String jid : jids) {
// Set the new affiliation.
MUCOwner.Item item = new MUCOwner.Item(affiliation);
item.setJid(jid);
iq.addItem(item);
}
// Wait for a response packet back from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the change request to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
}
/**
* Tries to change the affiliation with an 'muc#admin' namespace
*
* @param jid
* @param affiliation
* @param reason the reason for the affiliation change (optional)
* @throws XMPPException
*/
private void changeAffiliationByAdmin(String jid, String affiliation, String reason)
throws XMPPException {
MUCAdmin iq = new MUCAdmin();
iq.setTo(room);
iq.setType(IQ.Type.SET);
// Set the new affiliation.
MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
item.setJid(jid);
item.setReason(reason);
iq.addItem(item);
// Wait for a response packet back from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the change request to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
}
private void changeAffiliationByAdmin(Collection<String> jids, String affiliation)
throws XMPPException {
MUCAdmin iq = new MUCAdmin();
iq.setTo(room);
iq.setType(IQ.Type.SET);
for (String jid : jids) {
// Set the new affiliation.
MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
item.setJid(jid);
iq.addItem(item);
}
// Wait for a response packet back from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the change request to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
}
private void changeRole(String nickname, String role, String reason) throws XMPPException {
MUCAdmin iq = new MUCAdmin();
iq.setTo(room);
iq.setType(IQ.Type.SET);
// Set the new role.
MUCAdmin.Item item = new MUCAdmin.Item(null, role);
item.setNick(nickname);
item.setReason(reason);
iq.addItem(item);
// Wait for a response packet back from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the change request to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
}
private void changeRole(Collection<String> nicknames, String role) throws XMPPException {
MUCAdmin iq = new MUCAdmin();
iq.setTo(room);
iq.setType(IQ.Type.SET);
for (String nickname : nicknames) {
// Set the new role.
MUCAdmin.Item item = new MUCAdmin.Item(null, role);
item.setNick(nickname);
iq.addItem(item);
}
// Wait for a response packet back from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the change request to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
}
/**
* Returns the number of occupants in the group chat.<p>
*
* Note: this value will only be accurate after joining the group chat, and
* may fluctuate over time. If you query this value directly after joining the
* group chat it may not be accurate, as it takes a certain amount of time for
* the server to send all presence packets to this client.
*
* @return the number of occupants in the group chat.
*/
public int getOccupantsCount() {
return occupantsMap.size();
}
/**
* Returns an Iterator (of Strings) for the list of fully qualified occupants
* in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
* Typically, a client would only display the nickname of the occupant. To
* get the nickname from the fully qualified name, use the
* {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method.
* Note: this value will only be accurate after joining the group chat, and may
* fluctuate over time.
*
* @return an Iterator for the occupants in the group chat.
*/
public Iterator<String> getOccupants() {
return Collections.unmodifiableList(new ArrayList<String>(occupantsMap.keySet()))
.iterator();
}
/**
* Returns the presence info for a particular user, or <tt>null</tt> if the user
* is not in the room.<p>
*
* @param user the room occupant to search for his presence. The format of user must
* be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
* @return the occupant's current presence, or <tt>null</tt> if the user is unavailable
* or if no presence information is available.
*/
public Presence getOccupantPresence(String user) {
return occupantsMap.get(user);
}
/**
* Returns the Occupant information for a particular occupant, or <tt>null</tt> if the
* user is not in the room. The Occupant object may include information such as full
* JID of the user as well as the role and affiliation of the user in the room.<p>
*
* @param user the room occupant to search for his presence. The format of user must
* be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
* @return the Occupant or <tt>null</tt> if the user is unavailable (i.e. not in the room).
*/
public Occupant getOccupant(String user) {
Presence presence = occupantsMap.get(user);
if (presence != null) {
return new Occupant(presence);
}
return null;
}
/**
* Adds a packet listener that will be notified of any new Presence packets
* sent to the group chat. Using a listener is a suitable way to know when the list
* of occupants should be re-loaded due to any changes.
*
* @param listener a packet listener that will be notified of any presence packets
* sent to the group chat.
*/
public void addParticipantListener(PacketListener listener) {
connection.addPacketListener(listener, presenceFilter);
connectionListeners.add(listener);
}
/**
* Remoces a packet listener that was being notified of any new Presence packets
* sent to the group chat.
*
* @param listener a packet listener that was being notified of any presence packets
* sent to the group chat.
*/
public void removeParticipantListener(PacketListener listener) {
connection.removePacketListener(listener);
connectionListeners.remove(listener);
}
/**
* Returns a collection of <code>Affiliate</code> with the room owners.
*
* @return a collection of <code>Affiliate</code> with the room owners.
* @throws XMPPException if an error occured while performing the request to the server or you
* don't have enough privileges to get this information.
*/
public Collection<Affiliate> getOwners() throws XMPPException {
return getAffiliatesByAdmin("owner");
}
/**
* Returns a collection of <code>Affiliate</code> with the room administrators.
*
* @return a collection of <code>Affiliate</code> with the room administrators.
* @throws XMPPException if an error occured while performing the request to the server or you
* don't have enough privileges to get this information.
*/
public Collection<Affiliate> getAdmins() throws XMPPException {
return getAffiliatesByOwner("admin");
}
/**
* Returns a collection of <code>Affiliate</code> with the room members.
*
* @return a collection of <code>Affiliate</code> with the room members.
* @throws XMPPException if an error occured while performing the request to the server or you
* don't have enough privileges to get this information.
*/
public Collection<Affiliate> getMembers() throws XMPPException {
return getAffiliatesByAdmin("member");
}
/**
* Returns a collection of <code>Affiliate</code> with the room outcasts.
*
* @return a collection of <code>Affiliate</code> with the room outcasts.
* @throws XMPPException if an error occured while performing the request to the server or you
* don't have enough privileges to get this information.
*/
public Collection<Affiliate> getOutcasts() throws XMPPException {
return getAffiliatesByAdmin("outcast");
}
/**
* Returns a collection of <code>Affiliate</code> that have the specified room affiliation
* sending a request in the owner namespace.
*
* @param affiliation the affiliation of the users in the room.
* @return a collection of <code>Affiliate</code> that have the specified room affiliation.
* @throws XMPPException if an error occured while performing the request to the server or you
* don't have enough privileges to get this information.
*/
private Collection<Affiliate> getAffiliatesByOwner(String affiliation) throws XMPPException {
MUCOwner iq = new MUCOwner();
iq.setTo(room);
iq.setType(IQ.Type.GET);
// Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
MUCOwner.Item item = new MUCOwner.Item(affiliation);
iq.addItem(item);
// Wait for a response packet back from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the request to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
MUCOwner answer = (MUCOwner) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
// Get the list of affiliates from the server's answer
List<Affiliate> affiliates = new ArrayList<Affiliate>();
for (Iterator<MUCOwner.Item> it = answer.getItems(); it.hasNext();) {
affiliates.add(new Affiliate(it.next()));
}
return affiliates;
}
/**
* Returns a collection of <code>Affiliate</code> that have the specified room affiliation
* sending a request in the admin namespace.
*
* @param affiliation the affiliation of the users in the room.
* @return a collection of <code>Affiliate</code> that have the specified room affiliation.
* @throws XMPPException if an error occured while performing the request to the server or you
* don't have enough privileges to get this information.
*/
private Collection<Affiliate> getAffiliatesByAdmin(String affiliation) throws XMPPException {
MUCAdmin iq = new MUCAdmin();
iq.setTo(room);
iq.setType(IQ.Type.GET);
// Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
iq.addItem(item);
// Wait for a response packet back from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the request to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
// Get the list of affiliates from the server's answer
List<Affiliate> affiliates = new ArrayList<Affiliate>();
for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) {
affiliates.add(new Affiliate(it.next()));
}
return affiliates;
}
/**
* Returns a collection of <code>Occupant</code> with the room moderators.
*
* @return a collection of <code>Occupant</code> with the room moderators.
* @throws XMPPException if an error occured while performing the request to the server or you
* don't have enough privileges to get this information.
*/
public Collection<Occupant> getModerators() throws XMPPException {
return getOccupants("moderator");
}
/**
* Returns a collection of <code>Occupant</code> with the room participants.
*
* @return a collection of <code>Occupant</code> with the room participants.
* @throws XMPPException if an error occured while performing the request to the server or you
* don't have enough privileges to get this information.
*/
public Collection<Occupant> getParticipants() throws XMPPException {
return getOccupants("participant");
}
/**
* Returns a collection of <code>Occupant</code> that have the specified room role.
*
* @param role the role of the occupant in the room.
* @return a collection of <code>Occupant</code> that have the specified room role.
* @throws XMPPException if an error occured while performing the request to the server or you
* don't have enough privileges to get this information.
*/
private Collection<Occupant> getOccupants(String role) throws XMPPException {
MUCAdmin iq = new MUCAdmin();
iq.setTo(room);
iq.setType(IQ.Type.GET);
// Set the specified role. This may request the list of moderators/participants.
MUCAdmin.Item item = new MUCAdmin.Item(null, role);
iq.addItem(item);
// Wait for a response packet back from the server.
PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send the request to the server.
connection.sendPacket(iq);
// Wait up to a certain number of seconds for a reply.
MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
// Get the list of participants from the server's answer
List<Occupant> participants = new ArrayList<Occupant>();
for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) {
participants.add(new Occupant(it.next()));
}
return participants;
}
/**
* Sends a message to the chat room.
*
* @param text the text of the message to send.
* @throws XMPPException if sending the message fails.
*/
public void sendMessage(String text) throws XMPPException {
Message message = new Message(room, Message.Type.groupchat);
message.setBody(text);
connection.sendPacket(message);
}
/**
* Returns a new Chat for sending private messages to a given room occupant.
* The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
* service will change the 'from' address to the sender's room JID and delivering the message
* to the intended recipient's full JID.
*
* @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul').
* @param listener the listener is a message listener that will handle messages for the newly
* created chat.
* @return new Chat for sending private messages to a given room occupant.
*/
public Chat createPrivateChat(String occupant, MessageListener listener) {
return connection.getChatManager().createChat(occupant, listener);
}
/**
* Creates a new Message to send to the chat room.
*
* @return a new Message addressed to the chat room.
*/
public Message createMessage() {
return new Message(room, Message.Type.groupchat);
}
/**
* Sends a Message to the chat room.
*
* @param message the message.
* @throws XMPPException if sending the message fails.
*/
public void sendMessage(Message message) throws XMPPException {
connection.sendPacket(message);
}
/**
* Polls for and returns the next message, or <tt>null</tt> if there isn't
* a message immediately available. This method provides significantly different
* functionalty than the {@link #nextMessage()} method since it's non-blocking.
* In other words, the method call will always return immediately, whereas the
* nextMessage method will return only when a message is available (or after
* a specific timeout).
*
* @return the next message if one is immediately available and
* <tt>null</tt> otherwise.
*/
public Message pollMessage() {
return (Message) messageCollector.pollResult();
}
/**
* Returns the next available message in the chat. The method call will block
* (not return) until a message is available.
*
* @return the next message.
*/
public Message nextMessage() {
return (Message) messageCollector.nextResult();
}
/**
* Returns the next available message in the chat. The method call will block
* (not return) until a packet is available or the <tt>timeout</tt> has elapased.
* If the timeout elapses without a result, <tt>null</tt> will be returned.
*
* @param timeout the maximum amount of time to wait for the next message.
* @return the next message, or <tt>null</tt> if the timeout elapses without a
* message becoming available.
*/
public Message nextMessage(long timeout) {
return (Message) messageCollector.nextResult(timeout);
}
/**
* Adds a packet listener that will be notified of any new messages in the
* group chat. Only "group chat" messages addressed to this group chat will
* be delivered to the listener. If you wish to listen for other packets
* that may be associated with this group chat, you should register a
* PacketListener directly with the Connection with the appropriate
* PacketListener.
*
* @param listener a packet listener.
*/
public void addMessageListener(PacketListener listener) {
connection.addPacketListener(listener, messageFilter);
connectionListeners.add(listener);
}
/**
* Removes a packet listener that was being notified of any new messages in the
* multi user chat. Only "group chat" messages addressed to this multi user chat were
* being delivered to the listener.
*
* @param listener a packet listener.
*/
public void removeMessageListener(PacketListener listener) {
connection.removePacketListener(listener);
connectionListeners.remove(listener);
}
/**
* Changes the subject within the room. As a default, only users with a role of "moderator"
* are allowed to change the subject in a room. Although some rooms may be configured to
* allow a mere participant or even a visitor to change the subject.
*
* @param subject the new room's subject to set.
* @throws XMPPException if someone without appropriate privileges attempts to change the
* room subject will throw an error with code 403 (i.e. Forbidden)
*/
public void changeSubject(final String subject) throws XMPPException {
Message message = new Message(room, Message.Type.groupchat);
message.setSubject(subject);
// Wait for an error or confirmation message back from the server.
PacketFilter responseFilter =
new AndFilter(
new FromMatchesFilter(room),
new PacketTypeFilter(Message.class));
responseFilter = new AndFilter(responseFilter, new PacketFilter() {
public boolean accept(Packet packet) {
Message msg = (Message) packet;
return subject.equals(msg.getSubject());
}
});
PacketCollector response = connection.createPacketCollector(responseFilter);
// Send change subject packet.
connection.sendPacket(message);
// Wait up to a certain number of seconds for a reply.
Message answer =
(Message) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
// Stop queuing results
response.cancel();
if (answer == null) {
throw new XMPPException("No response from server.");
}
else if (answer.getError() != null) {
throw new XMPPException(answer.getError());
}
}
/**
* Notification message that the user has joined the room.
*/
private synchronized void userHasJoined() {
// Update the list of joined rooms through this connection
List<String> rooms = joinedRooms.get(connection);
if (rooms == null) {
rooms = new ArrayList<String>();
joinedRooms.put(connection, rooms);
}
rooms.add(room);
}
/**
* Notification message that the user has left the room.
*/
private synchronized void userHasLeft() {
// Update the list of joined rooms through this connection
List<String> rooms = joinedRooms.get(connection);
if (rooms == null) {
return;
}
rooms.remove(room);
cleanup();
}
/**
* Returns the MUCUser packet extension included in the packet or <tt>null</tt> if none.
*
* @param packet the packet that may include the MUCUser extension.
* @return the MUCUser found in the packet.
*/
private MUCUser getMUCUserExtension(Packet packet) {
if (packet != null) {
// Get the MUC User extension
return (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
}
return null;
}
/**
* Adds a listener that will be notified of changes in your status in the room
* such as the user being kicked, banned, or granted admin permissions.
*
* @param listener a user status listener.
*/
public void addUserStatusListener(UserStatusListener listener) {
synchronized (userStatusListeners) {
if (!userStatusListeners.contains(listener)) {
userStatusListeners.add(listener);
}
}
}
/**
* Removes a listener that was being notified of changes in your status in the room
* such as the user being kicked, banned, or granted admin permissions.
*
* @param listener a user status listener.
*/
public void removeUserStatusListener(UserStatusListener listener) {
synchronized (userStatusListeners) {
userStatusListeners.remove(listener);
}
}
private void fireUserStatusListeners(String methodName, Object[] params) {
UserStatusListener[] listeners;
synchronized (userStatusListeners) {
listeners = new UserStatusListener[userStatusListeners.size()];
userStatusListeners.toArray(listeners);
}
// Get the classes of the method parameters
Class<?>[] paramClasses = new Class[params.length];
for (int i = 0; i < params.length; i++) {
paramClasses[i] = params[i].getClass();
}
try {
// Get the method to execute based on the requested methodName and parameters classes
Method method = UserStatusListener.class.getDeclaredMethod(methodName, paramClasses);
for (UserStatusListener listener : listeners) {
method.invoke(listener, params);
}
} catch (NoSuchMethodException e) {
log.log(Level.SEVERE, "Failed to invoke method on UserStatusListener", e);
} catch (InvocationTargetException e) {
log.log(Level.SEVERE, "Failed to invoke method on UserStatusListener", e);
} catch (IllegalAccessException e) {
log.log(Level.SEVERE, "Failed to invoke method on UserStatusListener", e);
}
}
/**
* Adds a listener that will be notified of changes in occupants status in the room
* such as the user being kicked, banned, or granted admin permissions.
*
* @param listener a participant status listener.
*/
public void addParticipantStatusListener(ParticipantStatusListener listener) {
synchronized (participantStatusListeners) {
if (!participantStatusListeners.contains(listener)) {
participantStatusListeners.add(listener);
}
}
}
/**
* Removes a listener that was being notified of changes in occupants status in the room
* such as the user being kicked, banned, or granted admin permissions.
*
* @param listener a participant status listener.
*/
public void removeParticipantStatusListener(ParticipantStatusListener listener) {
synchronized (participantStatusListeners) {
participantStatusListeners.remove(listener);
}
}
private void fireParticipantStatusListeners(String methodName, List<String> params) {
ParticipantStatusListener[] listeners;
synchronized (participantStatusListeners) {
listeners = new ParticipantStatusListener[participantStatusListeners.size()];
participantStatusListeners.toArray(listeners);
}
try {
// Get the method to execute based on the requested methodName and parameter
Class<?>[] classes = new Class[params.size()];
for (int i=0;i<params.size(); i++) {
classes[i] = String.class;
}
Method method = ParticipantStatusListener.class.getDeclaredMethod(methodName, classes);
for (ParticipantStatusListener listener : listeners) {
method.invoke(listener, params.toArray());
}
} catch (NoSuchMethodException e) {
log.log(Level.SEVERE, "Failed to invoke method on ParticipantStatusListener", e);
} catch (InvocationTargetException e) {
log.log(Level.SEVERE, "Failed to invoke method on ParticipantStatusListener", e);
} catch (IllegalAccessException e) {
log.log(Level.SEVERE, "Failed to invoke method on ParticipantStatusListener", e);
}
}
private void init() {
// Create filters
messageFilter =
new AndFilter(
new FromMatchesFilter(room),
new MessageTypeFilter(Message.Type.groupchat));
messageFilter = new AndFilter(messageFilter, new PacketFilter() {
public boolean accept(Packet packet) {
Message msg = (Message) packet;
return msg.getBody() != null;
}
});
presenceFilter =
new AndFilter(new FromMatchesFilter(room), new PacketTypeFilter(Presence.class));
// Create a collector for incoming messages.
messageCollector = new ConnectionDetachedPacketCollector();
// Create a listener for subject updates.
PacketListener subjectListener = new PacketListener() {
public void processPacket(Packet packet) {
Message msg = (Message) packet;
// Update the room subject
subject = msg.getSubject();
// Fire event for subject updated listeners
fireSubjectUpdatedListeners(
msg.getSubject(),
msg.getFrom());
}
};
// Create a listener for all presence updates.
PacketListener presenceListener = new PacketListener() {
public void processPacket(Packet packet) {
Presence presence = (Presence) packet;
String from = presence.getFrom();
String myRoomJID = room + "/" + nickname;
boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
if (presence.getType() == Presence.Type.available) {
Presence oldPresence = occupantsMap.put(from, presence);
if (oldPresence != null) {
// Get the previous occupant's affiliation & role
MUCUser mucExtension = getMUCUserExtension(oldPresence);
String oldAffiliation = mucExtension.getItem().getAffiliation();
String oldRole = mucExtension.getItem().getRole();
// Get the new occupant's affiliation & role
mucExtension = getMUCUserExtension(presence);
String newAffiliation = mucExtension.getItem().getAffiliation();
String newRole = mucExtension.getItem().getRole();
// Fire role modification events
checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
// Fire affiliation modification events
checkAffiliationModifications(
oldAffiliation,
newAffiliation,
isUserStatusModification,
from);
}
else {
// A new occupant has joined the room
if (!isUserStatusModification) {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("joined", params);
}
}
}
else if (presence.getType() == Presence.Type.unavailable) {
occupantsMap.remove(from);
MUCUser mucUser = getMUCUserExtension(presence);
if (mucUser != null && mucUser.getStatus() != null) {
// Fire events according to the received presence code
checkPresenceCode(
mucUser.getStatus().getCode(),
presence.getFrom().equals(myRoomJID),
mucUser,
from);
} else {
// An occupant has left the room
if (!isUserStatusModification) {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("left", params);
}
}
}
}
};
// Listens for all messages that include a MUCUser extension and fire the invitation
// rejection listeners if the message includes an invitation rejection.
PacketListener declinesListener = new PacketListener() {
public void processPacket(Packet packet) {
// Get the MUC User extension
MUCUser mucUser = getMUCUserExtension(packet);
// Check if the MUCUser informs that the invitee has declined the invitation
if (mucUser.getDecline() != null &&
((Message) packet).getType() != Message.Type.error) {
// Fire event for invitation rejection listeners
fireInvitationRejectionListeners(
mucUser.getDecline().getFrom(),
mucUser.getDecline().getReason());
}
}
};
PacketMultiplexListener packetMultiplexor = new PacketMultiplexListener(
messageCollector, presenceListener, subjectListener,
declinesListener);
roomListenerMultiplexor = RoomListenerMultiplexor.getRoomMultiplexor(connection);
roomListenerMultiplexor.addRoom(room, packetMultiplexor);
}
/**
* Fires notification events if the role of a room occupant has changed. If the occupant that
* changed his role is your occupant then the <code>UserStatusListeners</code> added to this
* <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
* his role is not yours then the <code>ParticipantStatusListeners</code> added to this
* <code>MultiUserChat</code> will be fired. The following table shows the events that will
* be fired depending on the previous and new role of the occupant.
*
* <pre>
* <table border="1">
* <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
*
* <tr><td>None</td><td>Visitor</td><td>--</td></tr>
* <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
* <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
*
* <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
* <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
* <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
*
* <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
* <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
* <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
*
* <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
* <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
* <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
* </table>
* </pre>
*
* @param oldRole the previous role of the user in the room before receiving the new presence
* @param newRole the new role of the user in the room after receiving the new presence
* @param isUserModification whether the received presence is about your user in the room or not
* @param from the occupant whose role in the room has changed
* (e.g. room@conference.jabber.org/nick).
*/
private void checkRoleModifications(
String oldRole,
String newRole,
boolean isUserModification,
String from) {
// Voice was granted to a visitor
if (("visitor".equals(oldRole) || "none".equals(oldRole))
&& "participant".equals(newRole)) {
if (isUserModification) {
fireUserStatusListeners("voiceGranted", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("voiceGranted", params);
}
}
// The participant's voice was revoked from the room
else if (
"participant".equals(oldRole)
&& ("visitor".equals(newRole) || "none".equals(newRole))) {
if (isUserModification) {
fireUserStatusListeners("voiceRevoked", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("voiceRevoked", params);
}
}
// Moderator privileges were granted to a participant
if (!"moderator".equals(oldRole) && "moderator".equals(newRole)) {
if ("visitor".equals(oldRole) || "none".equals(oldRole)) {
if (isUserModification) {
fireUserStatusListeners("voiceGranted", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("voiceGranted", params);
}
}
if (isUserModification) {
fireUserStatusListeners("moderatorGranted", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("moderatorGranted", params);
}
}
// Moderator privileges were revoked from a participant
else if ("moderator".equals(oldRole) && !"moderator".equals(newRole)) {
if ("visitor".equals(newRole) || "none".equals(newRole)) {
if (isUserModification) {
fireUserStatusListeners("voiceRevoked", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("voiceRevoked", params);
}
}
if (isUserModification) {
fireUserStatusListeners("moderatorRevoked", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("moderatorRevoked", params);
}
}
}
/**
* Fires notification events if the affiliation of a room occupant has changed. If the
* occupant that changed his affiliation is your occupant then the
* <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
* On the other hand, if the occupant that changed his affiliation is not yours then the
* <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
* fired. The following table shows the events that will be fired depending on the previous
* and new affiliation of the occupant.
*
* <pre>
* <table border="1">
* <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
*
* <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
* <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
* <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
*
* <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
* <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
* <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
*
* <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
* <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
* <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
*
* <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
* <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
* <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
* <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
* </table>
* </pre>
*
* @param oldAffiliation the previous affiliation of the user in the room before receiving the
* new presence
* @param newAffiliation the new affiliation of the user in the room after receiving the new
* presence
* @param isUserModification whether the received presence is about your user in the room or not
* @param from the occupant whose role in the room has changed
* (e.g. room@conference.jabber.org/nick).
*/
private void checkAffiliationModifications(
String oldAffiliation,
String newAffiliation,
boolean isUserModification,
String from) {
// First check for revoked affiliation and then for granted affiliations. The idea is to
// first fire the "revoke" events and then fire the "grant" events.
// The user's ownership to the room was revoked
if ("owner".equals(oldAffiliation) && !"owner".equals(newAffiliation)) {
if (isUserModification) {
fireUserStatusListeners("ownershipRevoked", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("ownershipRevoked", params);
}
}
// The user's administrative privileges to the room were revoked
else if ("admin".equals(oldAffiliation) && !"admin".equals(newAffiliation)) {
if (isUserModification) {
fireUserStatusListeners("adminRevoked", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("adminRevoked", params);
}
}
// The user's membership to the room was revoked
else if ("member".equals(oldAffiliation) && !"member".equals(newAffiliation)) {
if (isUserModification) {
fireUserStatusListeners("membershipRevoked", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("membershipRevoked", params);
}
}
// The user was granted ownership to the room
if (!"owner".equals(oldAffiliation) && "owner".equals(newAffiliation)) {
if (isUserModification) {
fireUserStatusListeners("ownershipGranted", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("ownershipGranted", params);
}
}
// The user was granted administrative privileges to the room
else if (!"admin".equals(oldAffiliation) && "admin".equals(newAffiliation)) {
if (isUserModification) {
fireUserStatusListeners("adminGranted", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("adminGranted", params);
}
}
// The user was granted membership to the room
else if (!"member".equals(oldAffiliation) && "member".equals(newAffiliation)) {
if (isUserModification) {
fireUserStatusListeners("membershipGranted", new Object[] {});
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
fireParticipantStatusListeners("membershipGranted", params);
}
}
}
/**
* Fires events according to the received presence code.
*
* @param code
* @param isUserModification
* @param mucUser
* @param from
*/
private void checkPresenceCode(
String code,
boolean isUserModification,
MUCUser mucUser,
String from) {
// Check if an occupant was kicked from the room
if ("307".equals(code)) {
// Check if this occupant was kicked
if (isUserModification) {
joined = false;
fireUserStatusListeners(
"kicked",
new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
// Reset occupant information.
occupantsMap.clear();
nickname = null;
userHasLeft();
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
params.add(mucUser.getItem().getActor());
params.add(mucUser.getItem().getReason());
fireParticipantStatusListeners("kicked", params);
}
}
// A user was banned from the room
else if ("301".equals(code)) {
// Check if this occupant was banned
if (isUserModification) {
joined = false;
fireUserStatusListeners(
"banned",
new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
// Reset occupant information.
occupantsMap.clear();
nickname = null;
userHasLeft();
}
else {
List<String> params = new ArrayList<String>();
params.add(from);
params.add(mucUser.getItem().getActor());
params.add(mucUser.getItem().getReason());
fireParticipantStatusListeners("banned", params);
}
}
// A user's membership was revoked from the room
else if ("321".equals(code)) {
// Check if this occupant's membership was revoked
if (isUserModification) {
joined = false;
fireUserStatusListeners("membershipRevoked", new Object[] {});
// Reset occupant information.
occupantsMap.clear();
nickname = null;
userHasLeft();
}
}
// A occupant has changed his nickname in the room
else if ("303".equals(code)) {
List<String> params = new ArrayList<String>();
params.add(from);
params.add(mucUser.getItem().getNick());
fireParticipantStatusListeners("nicknameChanged", params);
}
}
private void cleanup() {
try {
if (connection != null) {
roomListenerMultiplexor.removeRoom(room);
// Remove all the PacketListeners added to the connection by this chat
for (PacketListener connectionListener : connectionListeners) {
connection.removePacketListener(connectionListener);
}
}
} catch (Exception e) {
// Do nothing
}
}
protected void finalize() throws Throwable {
cleanup();
super.finalize();
}
/**
* An InvitationsMonitor monitors a given connection to detect room invitations. Every
* time the InvitationsMonitor detects a new invitation it will fire the invitation listeners.
*
* @author Gaston Dombiak
*/
private static class InvitationsMonitor implements ConnectionListener {
// We use a WeakHashMap so that the GC can collect the monitor when the
// connection is no longer referenced by any object.
// Note that when the InvitationsMonitor is used, i.e. when there are InvitationListeners, it will add a
// PacketListener to the Connection and therefore a strong reference from the Connection to the
// InvitationsMonior will exists, preventing it from beeing gc'ed. After the last InvitationListener is gone,
// the PacketListener will get removed (cancel()) allowing the garbage collection of the InvitationsMonitor
// instance.
private final static Map<Connection, WeakReference<InvitationsMonitor>> monitors =
new WeakHashMap<Connection, WeakReference<InvitationsMonitor>>();
// We don't use a synchronized List here because it would break the semantic of (add|remove)InvitationListener
private final List<InvitationListener> invitationsListeners =
new ArrayList<InvitationListener>();
private Connection connection;
private PacketFilter invitationFilter;
private PacketListener invitationPacketListener;
/**
* Returns a new or existing InvitationsMonitor for a given connection.
*
* @param conn the connection to monitor for room invitations.
* @return a new or existing InvitationsMonitor for a given connection.
*/
public static InvitationsMonitor getInvitationsMonitor(Connection conn) {
synchronized (monitors) {
if (!monitors.containsKey(conn) || monitors.get(conn).get() == null) {
// We need to use a WeakReference because the monitor references the
// connection and this could prevent the GC from collecting the monitor
// when no other object references the monitor
InvitationsMonitor ivm = new InvitationsMonitor(conn);
monitors.put(conn, new WeakReference<InvitationsMonitor>(ivm));
return ivm;
}
// Return the InvitationsMonitor that monitors the connection
return monitors.get(conn).get();
}
}
/**
* Creates a new InvitationsMonitor that will monitor invitations received
* on a given connection.
*
* @param connection the connection to monitor for possible room invitations
*/
private InvitationsMonitor(Connection connection) {
this.connection = connection;
}
/**
* Adds a listener to invitation notifications. The listener will be fired anytime
* an invitation is received.<p>
*
* If this is the first monitor's listener then the monitor will be initialized in
* order to start listening to room invitations.
*
* @param listener an invitation listener.
*/
public void addInvitationListener(InvitationListener listener) {
synchronized (invitationsListeners) {
// If this is the first monitor's listener then initialize the listeners
// on the connection to detect room invitations
if (invitationsListeners.size() == 0) {
init();
}
if (!invitationsListeners.contains(listener)) {
invitationsListeners.add(listener);
}
}
}
/**
* Removes a listener to invitation notifications. The listener will be fired anytime
* an invitation is received.<p>
*
* If there are no more listeners to notifiy for room invitations then the monitor will
* be stopped. As soon as a new listener is added to the monitor, the monitor will resume
* monitoring the connection for new room invitations.
*
* @param listener an invitation listener.
*/
public void removeInvitationListener(InvitationListener listener) {
synchronized (invitationsListeners) {
if (invitationsListeners.contains(listener)) {
invitationsListeners.remove(listener);
}
// If there are no more listeners to notifiy for room invitations
// then proceed to cancel/release this monitor
if (invitationsListeners.size() == 0) {
cancel();
}
}
}
/**
* Fires invitation listeners.
*/
private void fireInvitationListeners(String room, String inviter, String reason, String password,
Message message) {
InvitationListener[] listeners;
synchronized (invitationsListeners) {
listeners = new InvitationListener[invitationsListeners.size()];
invitationsListeners.toArray(listeners);
}
for (InvitationListener listener : listeners) {
listener.invitationReceived(connection, room, inviter, reason, password, message);
}
}
public void connectionClosed() {
cancel();
}
public void connectionClosedOnError(Exception e) {
// ignore
}
public void reconnectingIn(int seconds) {
// ignore
}
public void reconnectionSuccessful() {
// ignore
}
public void reconnectionFailed(Exception e) {
// ignore
}
/**
* Initializes the listeners to detect received room invitations and to detect when the
* connection gets closed. As soon as a room invitation is received the invitations
* listeners will be fired. When the connection gets closed the monitor will remove
* his listeners on the connection.
*/
private void init() {
// Listens for all messages that include a MUCUser extension and fire the invitation
// listeners if the message includes an invitation.
invitationFilter =
new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user");
invitationPacketListener = new PacketListener() {
public void processPacket(Packet packet) {
// Get the MUCUser extension
MUCUser mucUser =
(MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
// Check if the MUCUser extension includes an invitation
if (mucUser.getInvite() != null &&
((Message) packet).getType() != Message.Type.error) {
// Fire event for invitation listeners
fireInvitationListeners(packet.getFrom(), mucUser.getInvite().getFrom(),
mucUser.getInvite().getReason(), mucUser.getPassword(), (Message) packet);
}
}
};
connection.addPacketListener(invitationPacketListener, invitationFilter);
// Add a listener to detect when the connection gets closed in order to
// cancel/release this monitor
connection.addConnectionListener(this);
}
/**
* Cancels all the listeners that this InvitationsMonitor has added to the connection.
*/
private void cancel() {
connection.removePacketListener(invitationPacketListener);
connection.removeConnectionListener(this);
}
}
}