Home | History | Annotate | Download | only in muc
      1 /**
      2  * $RCSfile$
      3  * $Revision$
      4  * $Date$
      5  *
      6  * Copyright 2003-2007 Jive Software.
      7  *
      8  * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
      9  * you may not use this file except in compliance with the License.
     10  * You may obtain a copy of the License at
     11  *
     12  *     http://www.apache.org/licenses/LICENSE-2.0
     13  *
     14  * Unless required by applicable law or agreed to in writing, software
     15  * distributed under the License is distributed on an "AS IS" BASIS,
     16  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     17  * See the License for the specific language governing permissions and
     18  * limitations under the License.
     19  */
     20 
     21 package org.jivesoftware.smackx.muc;
     22 
     23 import java.lang.ref.WeakReference;
     24 import java.lang.reflect.InvocationTargetException;
     25 import java.lang.reflect.Method;
     26 import java.util.ArrayList;
     27 import java.util.Collection;
     28 import java.util.Collections;
     29 import java.util.Iterator;
     30 import java.util.List;
     31 import java.util.Map;
     32 import java.util.WeakHashMap;
     33 import java.util.concurrent.ConcurrentHashMap;
     34 
     35 import org.jivesoftware.smack.Chat;
     36 import org.jivesoftware.smack.ConnectionCreationListener;
     37 import org.jivesoftware.smack.ConnectionListener;
     38 import org.jivesoftware.smack.MessageListener;
     39 import org.jivesoftware.smack.PacketCollector;
     40 import org.jivesoftware.smack.PacketInterceptor;
     41 import org.jivesoftware.smack.PacketListener;
     42 import org.jivesoftware.smack.SmackConfiguration;
     43 import org.jivesoftware.smack.Connection;
     44 import org.jivesoftware.smack.XMPPException;
     45 import org.jivesoftware.smack.filter.AndFilter;
     46 import org.jivesoftware.smack.filter.FromMatchesFilter;
     47 import org.jivesoftware.smack.filter.MessageTypeFilter;
     48 import org.jivesoftware.smack.filter.PacketExtensionFilter;
     49 import org.jivesoftware.smack.filter.PacketFilter;
     50 import org.jivesoftware.smack.filter.PacketIDFilter;
     51 import org.jivesoftware.smack.filter.PacketTypeFilter;
     52 import org.jivesoftware.smack.packet.IQ;
     53 import org.jivesoftware.smack.packet.Message;
     54 import org.jivesoftware.smack.packet.Packet;
     55 import org.jivesoftware.smack.packet.PacketExtension;
     56 import org.jivesoftware.smack.packet.Presence;
     57 import org.jivesoftware.smack.packet.Registration;
     58 import org.jivesoftware.smackx.Form;
     59 import org.jivesoftware.smackx.NodeInformationProvider;
     60 import org.jivesoftware.smackx.ServiceDiscoveryManager;
     61 import org.jivesoftware.smackx.packet.DiscoverInfo;
     62 import org.jivesoftware.smackx.packet.DiscoverItems;
     63 import org.jivesoftware.smackx.packet.MUCAdmin;
     64 import org.jivesoftware.smackx.packet.MUCInitialPresence;
     65 import org.jivesoftware.smackx.packet.MUCOwner;
     66 import org.jivesoftware.smackx.packet.MUCUser;
     67 
     68 /**
     69  * A MultiUserChat is a conversation that takes place among many users in a virtual
     70  * room. A room could have many occupants with different affiliation and roles.
     71  * Possible affiliatons are "owner", "admin", "member", and "outcast". Possible roles
     72  * are "moderator", "participant", and "visitor". Each role and affiliation guarantees
     73  * different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
     74  * Grant voice, Edit member list, etc.).
     75  *
     76  * @author Gaston Dombiak, Larry Kirschner
     77  */
     78 public class MultiUserChat {
     79 
     80     private final static String discoNamespace = "http://jabber.org/protocol/muc";
     81     private final static String discoNode = "http://jabber.org/protocol/muc#rooms";
     82 
     83     private static Map<Connection, List<String>> joinedRooms =
     84             new WeakHashMap<Connection, List<String>>();
     85 
     86     private Connection connection;
     87     private String room;
     88     private String subject;
     89     private String nickname = null;
     90     private boolean joined = false;
     91     private Map<String, Presence> occupantsMap = new ConcurrentHashMap<String, Presence>();
     92 
     93     private final List<InvitationRejectionListener> invitationRejectionListeners =
     94             new ArrayList<InvitationRejectionListener>();
     95     private final List<SubjectUpdatedListener> subjectUpdatedListeners =
     96             new ArrayList<SubjectUpdatedListener>();
     97     private final List<UserStatusListener> userStatusListeners =
     98             new ArrayList<UserStatusListener>();
     99     private final List<ParticipantStatusListener> participantStatusListeners =
    100             new ArrayList<ParticipantStatusListener>();
    101 
    102     private PacketFilter presenceFilter;
    103     private List<PacketInterceptor> presenceInterceptors = new ArrayList<PacketInterceptor>();
    104     private PacketFilter messageFilter;
    105     private RoomListenerMultiplexor roomListenerMultiplexor;
    106     private ConnectionDetachedPacketCollector messageCollector;
    107     private List<PacketListener> connectionListeners = new ArrayList<PacketListener>();
    108 
    109     static {
    110         Connection.addConnectionCreationListener(new ConnectionCreationListener() {
    111             public void connectionCreated(final Connection connection) {
    112                 // Set on every established connection that this client supports the Multi-User
    113                 // Chat protocol. This information will be used when another client tries to
    114                 // discover whether this client supports MUC or not.
    115                 ServiceDiscoveryManager.getInstanceFor(connection).addFeature(discoNamespace);
    116                 // Set the NodeInformationProvider that will provide information about the
    117                 // joined rooms whenever a disco request is received
    118                 ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(
    119                     discoNode,
    120                     new NodeInformationProvider() {
    121                         public List<DiscoverItems.Item> getNodeItems() {
    122                             List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
    123                             Iterator<String> rooms=MultiUserChat.getJoinedRooms(connection);
    124                             while (rooms.hasNext()) {
    125                                 answer.add(new DiscoverItems.Item(rooms.next()));
    126                             }
    127                             return answer;
    128                         }
    129 
    130                         public List<String> getNodeFeatures() {
    131                             return null;
    132                         }
    133 
    134                         public List<DiscoverInfo.Identity> getNodeIdentities() {
    135                             return null;
    136                         }
    137 
    138                         @Override
    139                         public List<PacketExtension> getNodePacketExtensions() {
    140                             return null;
    141                         }
    142                     });
    143             }
    144         });
    145     }
    146 
    147     /**
    148      * Creates a new multi user chat with the specified connection and room name. Note: no
    149      * information is sent to or received from the server until you attempt to
    150      * {@link #join(String) join} the chat room. On some server implementations,
    151      * the room will not be created until the first person joins it.<p>
    152      *
    153      * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com
    154      * for the XMPP server example.com). You must ensure that the room address you're
    155      * trying to connect to includes the proper chat sub-domain.
    156      *
    157      * @param connection the XMPP connection.
    158      * @param room the name of the room in the form "roomName@service", where
    159      *      "service" is the hostname at which the multi-user chat
    160      *      service is running. Make sure to provide a valid JID.
    161      */
    162     public MultiUserChat(Connection connection, String room) {
    163         this.connection = connection;
    164         this.room = room.toLowerCase();
    165         init();
    166     }
    167 
    168     /**
    169      * Returns true if the specified user supports the Multi-User Chat protocol.
    170      *
    171      * @param connection the connection to use to perform the service discovery.
    172      * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe (at) example.com.
    173      * @return a boolean indicating whether the specified user supports the MUC protocol.
    174      */
    175     public static boolean isServiceEnabled(Connection connection, String user) {
    176         try {
    177             DiscoverInfo result =
    178                 ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(user);
    179             return result.containsFeature(discoNamespace);
    180         }
    181         catch (XMPPException e) {
    182             e.printStackTrace();
    183             return false;
    184         }
    185     }
    186 
    187     /**
    188      * Returns an Iterator on the rooms where the user has joined using a given connection.
    189      * The Iterator will contain Strings where each String represents a room
    190      * (e.g. room (at) muc.jabber.org).
    191      *
    192      * @param connection the connection used to join the rooms.
    193      * @return an Iterator on the rooms where the user has joined using a given connection.
    194      */
    195     private static Iterator<String> getJoinedRooms(Connection connection) {
    196         List<String> rooms = joinedRooms.get(connection);
    197         if (rooms != null) {
    198             return rooms.iterator();
    199         }
    200         // Return an iterator on an empty collection (i.e. the user never joined a room)
    201         return new ArrayList<String>().iterator();
    202     }
    203 
    204     /**
    205      * Returns an Iterator on the rooms where the requested user has joined. The Iterator will
    206      * contain Strings where each String represents a room (e.g. room (at) muc.jabber.org).
    207      *
    208      * @param connection the connection to use to perform the service discovery.
    209      * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe (at) example.com.
    210      * @return an Iterator on the rooms where the requested user has joined.
    211      */
    212     public static Iterator<String> getJoinedRooms(Connection connection, String user) {
    213         try {
    214             ArrayList<String> answer = new ArrayList<String>();
    215             // Send the disco packet to the user
    216             DiscoverItems result =
    217                 ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(user, discoNode);
    218             // Collect the entityID for each returned item
    219             for (Iterator<DiscoverItems.Item> items=result.getItems(); items.hasNext();) {
    220                 answer.add(items.next().getEntityID());
    221             }
    222             return answer.iterator();
    223         }
    224         catch (XMPPException e) {
    225             e.printStackTrace();
    226             // Return an iterator on an empty collection
    227             return new ArrayList<String>().iterator();
    228         }
    229     }
    230 
    231     /**
    232      * Returns the discovered information of a given room without actually having to join the room.
    233      * The server will provide information only for rooms that are public.
    234      *
    235      * @param connection the XMPP connection to use for discovering information about the room.
    236      * @param room the name of the room in the form "roomName@service" of which we want to discover
    237      *        its information.
    238      * @return the discovered information of a given room without actually having to join the room.
    239      * @throws XMPPException if an error occured while trying to discover information of a room.
    240      */
    241     public static RoomInfo getRoomInfo(Connection connection, String room)
    242             throws XMPPException {
    243         DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(room);
    244         return new RoomInfo(info);
    245     }
    246 
    247     /**
    248      * Returns a collection with the XMPP addresses of the Multi-User Chat services.
    249      *
    250      * @param connection the XMPP connection to use for discovering Multi-User Chat services.
    251      * @return a collection with the XMPP addresses of the Multi-User Chat services.
    252      * @throws XMPPException if an error occured while trying to discover MUC services.
    253      */
    254     public static Collection<String> getServiceNames(Connection connection) throws XMPPException {
    255         final List<String> answer = new ArrayList<String>();
    256         ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
    257         DiscoverItems items = discoManager.discoverItems(connection.getServiceName());
    258         for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
    259             DiscoverItems.Item item = it.next();
    260             try {
    261                 DiscoverInfo info = discoManager.discoverInfo(item.getEntityID());
    262                 if (info.containsFeature("http://jabber.org/protocol/muc")) {
    263                     answer.add(item.getEntityID());
    264                 }
    265             }
    266             catch (XMPPException e) {
    267                 // Trouble finding info in some cases. This is a workaround for
    268                 // discovering info on remote servers.
    269             }
    270         }
    271         return answer;
    272     }
    273 
    274     /**
    275      * Returns a collection of HostedRooms where each HostedRoom has the XMPP address of the room
    276      * and the room's name. Once discovered the rooms hosted by a chat service it is possible to
    277      * discover more detailed room information or join the room.
    278      *
    279      * @param connection the XMPP connection to use for discovering hosted rooms by the MUC service.
    280      * @param serviceName the service that is hosting the rooms to discover.
    281      * @return a collection of HostedRooms.
    282      * @throws XMPPException if an error occured while trying to discover the information.
    283      */
    284     public static Collection<HostedRoom> getHostedRooms(Connection connection, String serviceName)
    285             throws XMPPException {
    286         List<HostedRoom> answer = new ArrayList<HostedRoom>();
    287         ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
    288         DiscoverItems items = discoManager.discoverItems(serviceName);
    289         for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
    290             answer.add(new HostedRoom(it.next()));
    291         }
    292         return answer;
    293     }
    294 
    295     /**
    296      * Returns the name of the room this MultiUserChat object represents.
    297      *
    298      * @return the multi user chat room name.
    299      */
    300     public String getRoom() {
    301         return room;
    302     }
    303 
    304     /**
    305      * Creates the room according to some default configuration, assign the requesting user
    306      * as the room owner, and add the owner to the room but not allow anyone else to enter
    307      * the room (effectively "locking" the room). The requesting user will join the room
    308      * under the specified nickname as soon as the room has been created.<p>
    309      *
    310      * To create an "Instant Room", that means a room with some default configuration that is
    311      * available for immediate access, the room's owner should send an empty form after creating
    312      * the room. {@link #sendConfigurationForm(Form)}<p>
    313      *
    314      * To create a "Reserved Room", that means a room manually configured by the room creator
    315      * before anyone is allowed to enter, the room's owner should complete and send a form after
    316      * creating the room. Once the completed configutation form is sent to the server, the server
    317      * will unlock the room. {@link #sendConfigurationForm(Form)}
    318      *
    319      * @param nickname the nickname to use.
    320      * @throws XMPPException if the room couldn't be created for some reason
    321      *          (e.g. room already exists; user already joined to an existant room or
    322      *          405 error if the user is not allowed to create the room)
    323      */
    324     public synchronized void create(String nickname) throws XMPPException {
    325         if (nickname == null || nickname.equals("")) {
    326             throw new IllegalArgumentException("Nickname must not be null or blank.");
    327         }
    328         // If we've already joined the room, leave it before joining under a new
    329         // nickname.
    330         if (joined) {
    331             throw new IllegalStateException("Creation failed - User already joined the room.");
    332         }
    333         // We create a room by sending a presence packet to room@service/nick
    334         // and signal support for MUC. The owner will be automatically logged into the room.
    335         Presence joinPresence = new Presence(Presence.Type.available);
    336         joinPresence.setTo(room + "/" + nickname);
    337         // Indicate the the client supports MUC
    338         joinPresence.addExtension(new MUCInitialPresence());
    339         // Invoke presence interceptors so that extra information can be dynamically added
    340         for (PacketInterceptor packetInterceptor : presenceInterceptors) {
    341             packetInterceptor.interceptPacket(joinPresence);
    342         }
    343 
    344         // Wait for a presence packet back from the server.
    345         PacketFilter responseFilter =
    346             new AndFilter(
    347                 new FromMatchesFilter(room + "/" + nickname),
    348                 new PacketTypeFilter(Presence.class));
    349         PacketCollector response = connection.createPacketCollector(responseFilter);
    350         // Send create & join packet.
    351         connection.sendPacket(joinPresence);
    352         // Wait up to a certain number of seconds for a reply.
    353         Presence presence =
    354             (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
    355         // Stop queuing results
    356         response.cancel();
    357 
    358         if (presence == null) {
    359             throw new XMPPException("No response from server.");
    360         }
    361         else if (presence.getError() != null) {
    362             throw new XMPPException(presence.getError());
    363         }
    364         // Whether the room existed before or was created, the user has joined the room
    365         this.nickname = nickname;
    366         joined = true;
    367         userHasJoined();
    368 
    369         // Look for confirmation of room creation from the server
    370         MUCUser mucUser = getMUCUserExtension(presence);
    371         if (mucUser != null && mucUser.getStatus() != null) {
    372             if ("201".equals(mucUser.getStatus().getCode())) {
    373                 // Room was created and the user has joined the room
    374                 return;
    375             }
    376         }
    377         // We need to leave the room since it seems that the room already existed
    378         leave();
    379         throw new XMPPException("Creation failed - Missing acknowledge of room creation.");
    380     }
    381 
    382     /**
    383      * Joins the chat room using the specified nickname. If already joined
    384      * using another nickname, this method will first leave the room and then
    385      * re-join using the new nickname. The default timeout of Smack for a reply
    386      * from the group chat server that the join succeeded will be used. After
    387      * joining the room, the room will decide the amount of history to send.
    388      *
    389      * @param nickname the nickname to use.
    390      * @throws XMPPException if an error occurs joining the room. In particular, a
    391      *      401 error can occur if no password was provided and one is required; or a
    392      *      403 error can occur if the user is banned; or a
    393      *      404 error can occur if the room does not exist or is locked; or a
    394      *      407 error can occur if user is not on the member list; or a
    395      *      409 error can occur if someone is already in the group chat with the same nickname.
    396      */
    397     public void join(String nickname) throws XMPPException {
    398         join(nickname, null, null, SmackConfiguration.getPacketReplyTimeout());
    399     }
    400 
    401     /**
    402      * Joins the chat room using the specified nickname and password. If already joined
    403      * using another nickname, this method will first leave the room and then
    404      * re-join using the new nickname. The default timeout of Smack for a reply
    405      * from the group chat server that the join succeeded will be used. After
    406      * joining the room, the room will decide the amount of history to send.<p>
    407      *
    408      * A password is required when joining password protected rooms. If the room does
    409      * not require a password there is no need to provide one.
    410      *
    411      * @param nickname the nickname to use.
    412      * @param password the password to use.
    413      * @throws XMPPException if an error occurs joining the room. In particular, a
    414      *      401 error can occur if no password was provided and one is required; or a
    415      *      403 error can occur if the user is banned; or a
    416      *      404 error can occur if the room does not exist or is locked; or a
    417      *      407 error can occur if user is not on the member list; or a
    418      *      409 error can occur if someone is already in the group chat with the same nickname.
    419      */
    420     public void join(String nickname, String password) throws XMPPException {
    421         join(nickname, password, null, SmackConfiguration.getPacketReplyTimeout());
    422     }
    423 
    424     /**
    425      * Joins the chat room using the specified nickname and password. If already joined
    426      * using another nickname, this method will first leave the room and then
    427      * re-join using the new nickname.<p>
    428      *
    429      * To control the amount of history to receive while joining a room you will need to provide
    430      * a configured DiscussionHistory object.<p>
    431      *
    432      * A password is required when joining password protected rooms. If the room does
    433      * not require a password there is no need to provide one.<p>
    434      *
    435      * If the room does not already exist when the user seeks to enter it, the server will
    436      * decide to create a new room or not.
    437      *
    438      * @param nickname the nickname to use.
    439      * @param password the password to use.
    440      * @param history the amount of discussion history to receive while joining a room.
    441      * @param timeout the amount of time to wait for a reply from the MUC service(in milleseconds).
    442      * @throws XMPPException if an error occurs joining the room. In particular, a
    443      *      401 error can occur if no password was provided and one is required; or a
    444      *      403 error can occur if the user is banned; or a
    445      *      404 error can occur if the room does not exist or is locked; or a
    446      *      407 error can occur if user is not on the member list; or a
    447      *      409 error can occur if someone is already in the group chat with the same nickname.
    448      */
    449     public synchronized void join(
    450         String nickname,
    451         String password,
    452         DiscussionHistory history,
    453         long timeout)
    454         throws XMPPException {
    455         if (nickname == null || nickname.equals("")) {
    456             throw new IllegalArgumentException("Nickname must not be null or blank.");
    457         }
    458         // If we've already joined the room, leave it before joining under a new
    459         // nickname.
    460         if (joined) {
    461             leave();
    462         }
    463         // We join a room by sending a presence packet where the "to"
    464         // field is in the form "roomName@service/nickname"
    465         Presence joinPresence = new Presence(Presence.Type.available);
    466         joinPresence.setTo(room + "/" + nickname);
    467 
    468         // Indicate the the client supports MUC
    469         MUCInitialPresence mucInitialPresence = new MUCInitialPresence();
    470         if (password != null) {
    471             mucInitialPresence.setPassword(password);
    472         }
    473         if (history != null) {
    474             mucInitialPresence.setHistory(history.getMUCHistory());
    475         }
    476         joinPresence.addExtension(mucInitialPresence);
    477         // Invoke presence interceptors so that extra information can be dynamically added
    478         for (PacketInterceptor packetInterceptor : presenceInterceptors) {
    479             packetInterceptor.interceptPacket(joinPresence);
    480         }
    481 
    482         // Wait for a presence packet back from the server.
    483         PacketFilter responseFilter =
    484                 new AndFilter(
    485                         new FromMatchesFilter(room + "/" + nickname),
    486                         new PacketTypeFilter(Presence.class));
    487         PacketCollector response = null;
    488         Presence presence;
    489         try {
    490             response = connection.createPacketCollector(responseFilter);
    491             // Send join packet.
    492             connection.sendPacket(joinPresence);
    493             // Wait up to a certain number of seconds for a reply.
    494             presence = (Presence) response.nextResult(timeout);
    495         }
    496         finally {
    497             // Stop queuing results
    498             if (response != null) {
    499                 response.cancel();
    500             }
    501         }
    502 
    503         if (presence == null) {
    504             throw new XMPPException("No response from server.");
    505         }
    506         else if (presence.getError() != null) {
    507             throw new XMPPException(presence.getError());
    508         }
    509         this.nickname = nickname;
    510         joined = true;
    511         userHasJoined();
    512     }
    513 
    514     /**
    515      * Returns true if currently in the multi user chat (after calling the {@link
    516      * #join(String)} method).
    517      *
    518      * @return true if currently in the multi user chat room.
    519      */
    520     public boolean isJoined() {
    521         return joined;
    522     }
    523 
    524     /**
    525      * Leave the chat room.
    526      */
    527     public synchronized void leave() {
    528         // If not joined already, do nothing.
    529         if (!joined) {
    530             return;
    531         }
    532         // We leave a room by sending a presence packet where the "to"
    533         // field is in the form "roomName@service/nickname"
    534         Presence leavePresence = new Presence(Presence.Type.unavailable);
    535         leavePresence.setTo(room + "/" + nickname);
    536         // Invoke presence interceptors so that extra information can be dynamically added
    537         for (PacketInterceptor packetInterceptor : presenceInterceptors) {
    538             packetInterceptor.interceptPacket(leavePresence);
    539         }
    540         connection.sendPacket(leavePresence);
    541         // Reset occupant information.
    542         occupantsMap.clear();
    543         nickname = null;
    544         joined = false;
    545         userHasLeft();
    546     }
    547 
    548     /**
    549      * Returns the room's configuration form that the room's owner can use or <tt>null</tt> if
    550      * no configuration is possible. The configuration form allows to set the room's language,
    551      * enable logging, specify room's type, etc..
    552      *
    553      * @return the Form that contains the fields to complete together with the instrucions or
    554      * <tt>null</tt> if no configuration is possible.
    555      * @throws XMPPException if an error occurs asking the configuration form for the room.
    556      */
    557     public Form getConfigurationForm() throws XMPPException {
    558         MUCOwner iq = new MUCOwner();
    559         iq.setTo(room);
    560         iq.setType(IQ.Type.GET);
    561 
    562         // Filter packets looking for an answer from the server.
    563         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
    564         PacketCollector response = connection.createPacketCollector(responseFilter);
    565         // Request the configuration form to the server.
    566         connection.sendPacket(iq);
    567         // Wait up to a certain number of seconds for a reply.
    568         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
    569         // Stop queuing results
    570         response.cancel();
    571 
    572         if (answer == null) {
    573             throw new XMPPException("No response from server.");
    574         }
    575         else if (answer.getError() != null) {
    576             throw new XMPPException(answer.getError());
    577         }
    578         return Form.getFormFrom(answer);
    579     }
    580 
    581     /**
    582      * Sends the completed configuration form to the server. The room will be configured
    583      * with the new settings defined in the form. If the form is empty then the server
    584      * will create an instant room (will use default configuration).
    585      *
    586      * @param form the form with the new settings.
    587      * @throws XMPPException if an error occurs setting the new rooms' configuration.
    588      */
    589     public void sendConfigurationForm(Form form) throws XMPPException {
    590         MUCOwner iq = new MUCOwner();
    591         iq.setTo(room);
    592         iq.setType(IQ.Type.SET);
    593         iq.addExtension(form.getDataFormToSend());
    594 
    595         // Filter packets looking for an answer from the server.
    596         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
    597         PacketCollector response = connection.createPacketCollector(responseFilter);
    598         // Send the completed configuration form to the server.
    599         connection.sendPacket(iq);
    600         // Wait up to a certain number of seconds for a reply.
    601         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
    602         // Stop queuing results
    603         response.cancel();
    604 
    605         if (answer == null) {
    606             throw new XMPPException("No response from server.");
    607         }
    608         else if (answer.getError() != null) {
    609             throw new XMPPException(answer.getError());
    610         }
    611     }
    612 
    613     /**
    614      * Returns the room's registration form that an unaffiliated user, can use to become a member
    615      * of the room or <tt>null</tt> if no registration is possible. Some rooms may restrict the
    616      * privilege to register members and allow only room admins to add new members.<p>
    617      *
    618      * If the user requesting registration requirements is not allowed to register with the room
    619      * (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
    620      * error to the user (error code 405).
    621      *
    622      * @return the registration Form that contains the fields to complete together with the
    623      * instrucions or <tt>null</tt> if no registration is possible.
    624      * @throws XMPPException if an error occurs asking the registration form for the room or a
    625      * 405 error if the user is not allowed to register with the room.
    626      */
    627     public Form getRegistrationForm() throws XMPPException {
    628         Registration reg = new Registration();
    629         reg.setType(IQ.Type.GET);
    630         reg.setTo(room);
    631 
    632         PacketFilter filter =
    633             new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
    634         PacketCollector collector = connection.createPacketCollector(filter);
    635         connection.sendPacket(reg);
    636         IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    637         collector.cancel();
    638         if (result == null) {
    639             throw new XMPPException("No response from server.");
    640         }
    641         else if (result.getType() == IQ.Type.ERROR) {
    642             throw new XMPPException(result.getError());
    643         }
    644         return Form.getFormFrom(result);
    645     }
    646 
    647     /**
    648      * Sends the completed registration form to the server. After the user successfully submits
    649      * the form, the room may queue the request for review by the room admins or may immediately
    650      * add the user to the member list by changing the user's affiliation from "none" to "member.<p>
    651      *
    652      * If the desired room nickname is already reserved for that room, the room will return a
    653      * "Conflict" error to the user (error code 409). If the room does not support registration,
    654      * it will return a "Service Unavailable" error to the user (error code 503).
    655      *
    656      * @param form the completed registration form.
    657      * @throws XMPPException if an error occurs submitting the registration form. In particular, a
    658      *      409 error can occur if the desired room nickname is already reserved for that room;
    659      *      or a 503 error can occur if the room does not support registration.
    660      */
    661     public void sendRegistrationForm(Form form) throws XMPPException {
    662         Registration reg = new Registration();
    663         reg.setType(IQ.Type.SET);
    664         reg.setTo(room);
    665         reg.addExtension(form.getDataFormToSend());
    666 
    667         PacketFilter filter =
    668             new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
    669         PacketCollector collector = connection.createPacketCollector(filter);
    670         connection.sendPacket(reg);
    671         IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
    672         collector.cancel();
    673         if (result == null) {
    674             throw new XMPPException("No response from server.");
    675         }
    676         else if (result.getType() == IQ.Type.ERROR) {
    677             throw new XMPPException(result.getError());
    678         }
    679     }
    680 
    681     /**
    682      * Sends a request to the server to destroy the room. The sender of the request
    683      * should be the room's owner. If the sender of the destroy request is not the room's owner
    684      * then the server will answer a "Forbidden" error (403).
    685      *
    686      * @param reason the reason for the room destruction.
    687      * @param alternateJID the JID of an alternate location.
    688      * @throws XMPPException if an error occurs while trying to destroy the room.
    689      *      An error can occur which will be wrapped by an XMPPException --
    690      *      XMPP error code 403. The error code can be used to present more
    691      *      appropiate error messages to end-users.
    692      */
    693     public void destroy(String reason, String alternateJID) throws XMPPException {
    694         MUCOwner iq = new MUCOwner();
    695         iq.setTo(room);
    696         iq.setType(IQ.Type.SET);
    697 
    698         // Create the reason for the room destruction
    699         MUCOwner.Destroy destroy = new MUCOwner.Destroy();
    700         destroy.setReason(reason);
    701         destroy.setJid(alternateJID);
    702         iq.setDestroy(destroy);
    703 
    704         // Wait for a presence packet back from the server.
    705         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
    706         PacketCollector response = connection.createPacketCollector(responseFilter);
    707         // Send the room destruction request.
    708         connection.sendPacket(iq);
    709         // Wait up to a certain number of seconds for a reply.
    710         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
    711         // Stop queuing results
    712         response.cancel();
    713 
    714         if (answer == null) {
    715             throw new XMPPException("No response from server.");
    716         }
    717         else if (answer.getError() != null) {
    718             throw new XMPPException(answer.getError());
    719         }
    720         // Reset occupant information.
    721         occupantsMap.clear();
    722         nickname = null;
    723         joined = false;
    724         userHasLeft();
    725     }
    726 
    727     /**
    728      * Invites another user to the room in which one is an occupant. The invitation
    729      * will be sent to the room which in turn will forward the invitation to the invitee.<p>
    730      *
    731      * If the room is password-protected, the invitee will receive a password to use to join
    732      * the room. If the room is members-only, the the invitee may be added to the member list.
    733      *
    734      * @param user the user to invite to the room.(e.g. hecate (at) shakespeare.lit)
    735      * @param reason the reason why the user is being invited.
    736      */
    737     public void invite(String user, String reason) {
    738         invite(new Message(), user, reason);
    739     }
    740 
    741     /**
    742      * Invites another user to the room in which one is an occupant using a given Message. The invitation
    743      * will be sent to the room which in turn will forward the invitation to the invitee.<p>
    744      *
    745      * If the room is password-protected, the invitee will receive a password to use to join
    746      * the room. If the room is members-only, the the invitee may be added to the member list.
    747      *
    748      * @param message the message to use for sending the invitation.
    749      * @param user the user to invite to the room.(e.g. hecate (at) shakespeare.lit)
    750      * @param reason the reason why the user is being invited.
    751      */
    752     public void invite(Message message, String user, String reason) {
    753         // TODO listen for 404 error code when inviter supplies a non-existent JID
    754         message.setTo(room);
    755 
    756         // Create the MUCUser packet that will include the invitation
    757         MUCUser mucUser = new MUCUser();
    758         MUCUser.Invite invite = new MUCUser.Invite();
    759         invite.setTo(user);
    760         invite.setReason(reason);
    761         mucUser.setInvite(invite);
    762         // Add the MUCUser packet that includes the invitation to the message
    763         message.addExtension(mucUser);
    764 
    765         connection.sendPacket(message);
    766     }
    767 
    768     /**
    769      * Informs the sender of an invitation that the invitee declines the invitation. The rejection
    770      * will be sent to the room which in turn will forward the rejection to the inviter.
    771      *
    772      * @param conn the connection to use for sending the rejection.
    773      * @param room the room that sent the original invitation.
    774      * @param inviter the inviter of the declined invitation.
    775      * @param reason the reason why the invitee is declining the invitation.
    776      */
    777     public static void decline(Connection conn, String room, String inviter, String reason) {
    778         Message message = new Message(room);
    779 
    780         // Create the MUCUser packet that will include the rejection
    781         MUCUser mucUser = new MUCUser();
    782         MUCUser.Decline decline = new MUCUser.Decline();
    783         decline.setTo(inviter);
    784         decline.setReason(reason);
    785         mucUser.setDecline(decline);
    786         // Add the MUCUser packet that includes the rejection
    787         message.addExtension(mucUser);
    788 
    789         conn.sendPacket(message);
    790     }
    791 
    792     /**
    793      * Adds a listener to invitation notifications. The listener will be fired anytime
    794      * an invitation is received.
    795      *
    796      * @param conn the connection where the listener will be applied.
    797      * @param listener an invitation listener.
    798      */
    799     public static void addInvitationListener(Connection conn, InvitationListener listener) {
    800         InvitationsMonitor.getInvitationsMonitor(conn).addInvitationListener(listener);
    801     }
    802 
    803     /**
    804      * Removes a listener to invitation notifications. The listener will be fired anytime
    805      * an invitation is received.
    806      *
    807      * @param conn the connection where the listener was applied.
    808      * @param listener an invitation listener.
    809      */
    810     public static void removeInvitationListener(Connection conn, InvitationListener listener) {
    811         InvitationsMonitor.getInvitationsMonitor(conn).removeInvitationListener(listener);
    812     }
    813 
    814     /**
    815      * Adds a listener to invitation rejections notifications. The listener will be fired anytime
    816      * an invitation is declined.
    817      *
    818      * @param listener an invitation rejection listener.
    819      */
    820     public void addInvitationRejectionListener(InvitationRejectionListener listener) {
    821         synchronized (invitationRejectionListeners) {
    822             if (!invitationRejectionListeners.contains(listener)) {
    823                 invitationRejectionListeners.add(listener);
    824             }
    825         }
    826     }
    827 
    828     /**
    829      * Removes a listener from invitation rejections notifications. The listener will be fired
    830      * anytime an invitation is declined.
    831      *
    832      * @param listener an invitation rejection listener.
    833      */
    834     public void removeInvitationRejectionListener(InvitationRejectionListener listener) {
    835         synchronized (invitationRejectionListeners) {
    836             invitationRejectionListeners.remove(listener);
    837         }
    838     }
    839 
    840     /**
    841      * Fires invitation rejection listeners.
    842      *
    843      * @param invitee the user being invited.
    844      * @param reason the reason for the rejection
    845      */
    846     private void fireInvitationRejectionListeners(String invitee, String reason) {
    847         InvitationRejectionListener[] listeners;
    848         synchronized (invitationRejectionListeners) {
    849             listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
    850             invitationRejectionListeners.toArray(listeners);
    851         }
    852         for (InvitationRejectionListener listener : listeners) {
    853             listener.invitationDeclined(invitee, reason);
    854         }
    855     }
    856 
    857     /**
    858      * Adds a listener to subject change notifications. The listener will be fired anytime
    859      * the room's subject changes.
    860      *
    861      * @param listener a subject updated listener.
    862      */
    863     public void addSubjectUpdatedListener(SubjectUpdatedListener listener) {
    864         synchronized (subjectUpdatedListeners) {
    865             if (!subjectUpdatedListeners.contains(listener)) {
    866                 subjectUpdatedListeners.add(listener);
    867             }
    868         }
    869     }
    870 
    871     /**
    872      * Removes a listener from subject change notifications. The listener will be fired
    873      * anytime the room's subject changes.
    874      *
    875      * @param listener a subject updated listener.
    876      */
    877     public void removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
    878         synchronized (subjectUpdatedListeners) {
    879             subjectUpdatedListeners.remove(listener);
    880         }
    881     }
    882 
    883     /**
    884      * Fires subject updated listeners.
    885      */
    886     private void fireSubjectUpdatedListeners(String subject, String from) {
    887         SubjectUpdatedListener[] listeners;
    888         synchronized (subjectUpdatedListeners) {
    889             listeners = new SubjectUpdatedListener[subjectUpdatedListeners.size()];
    890             subjectUpdatedListeners.toArray(listeners);
    891         }
    892         for (SubjectUpdatedListener listener : listeners) {
    893             listener.subjectUpdated(subject, from);
    894         }
    895     }
    896 
    897     /**
    898      * Adds a new {@link PacketInterceptor} that will be invoked every time a new presence
    899      * is going to be sent by this MultiUserChat to the server. Packet interceptors may
    900      * add new extensions to the presence that is going to be sent to the MUC service.
    901      *
    902      * @param presenceInterceptor the new packet interceptor that will intercept presence packets.
    903      */
    904     public void addPresenceInterceptor(PacketInterceptor presenceInterceptor) {
    905         presenceInterceptors.add(presenceInterceptor);
    906     }
    907 
    908     /**
    909      * Removes a {@link PacketInterceptor} that was being invoked every time a new presence
    910      * was being sent by this MultiUserChat to the server. Packet interceptors may
    911      * add new extensions to the presence that is going to be sent to the MUC service.
    912      *
    913      * @param presenceInterceptor the packet interceptor to remove.
    914      */
    915     public void removePresenceInterceptor(PacketInterceptor presenceInterceptor) {
    916         presenceInterceptors.remove(presenceInterceptor);
    917     }
    918 
    919     /**
    920      * Returns the last known room's subject or <tt>null</tt> if the user hasn't joined the room
    921      * or the room does not have a subject yet. In case the room has a subject, as soon as the
    922      * user joins the room a message with the current room's subject will be received.<p>
    923      *
    924      * To be notified every time the room's subject change you should add a listener
    925      * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
    926      *
    927      * To change the room's subject use {@link #changeSubject(String)}.
    928      *
    929      * @return the room's subject or <tt>null</tt> if the user hasn't joined the room or the
    930      * room does not have a subject yet.
    931      */
    932     public String getSubject() {
    933         return subject;
    934     }
    935 
    936     /**
    937      * Returns the reserved room nickname for the user in the room. A user may have a reserved
    938      * nickname, for example through explicit room registration or database integration. In such
    939      * cases it may be desirable for the user to discover the reserved nickname before attempting
    940      * to enter the room.
    941      *
    942      * @return the reserved room nickname or <tt>null</tt> if none.
    943      */
    944     public String getReservedNickname() {
    945         try {
    946             DiscoverInfo result =
    947                 ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
    948                     room,
    949                     "x-roomuser-item");
    950             // Look for an Identity that holds the reserved nickname and return its name
    951             for (Iterator<DiscoverInfo.Identity> identities = result.getIdentities();
    952                  identities.hasNext();) {
    953                 DiscoverInfo.Identity identity = identities.next();
    954                 return identity.getName();
    955             }
    956             // If no Identity was found then the user does not have a reserved room nickname
    957             return null;
    958         }
    959         catch (XMPPException e) {
    960             e.printStackTrace();
    961             return null;
    962         }
    963     }
    964 
    965     /**
    966      * Returns the nickname that was used to join the room, or <tt>null</tt> if not
    967      * currently joined.
    968      *
    969      * @return the nickname currently being used.
    970      */
    971     public String getNickname() {
    972         return nickname;
    973     }
    974 
    975     /**
    976      * Changes the occupant's nickname to a new nickname within the room. Each room occupant
    977      * will receive two presence packets. One of type "unavailable" for the old nickname and one
    978      * indicating availability for the new nickname. The unavailable presence will contain the new
    979      * nickname and an appropriate status code (namely 303) as extended presence information. The
    980      * status code 303 indicates that the occupant is changing his/her nickname.
    981      *
    982      * @param nickname the new nickname within the room.
    983      * @throws XMPPException if the new nickname is already in use by another occupant.
    984      */
    985     public void changeNickname(String nickname) throws XMPPException {
    986         if (nickname == null || nickname.equals("")) {
    987             throw new IllegalArgumentException("Nickname must not be null or blank.");
    988         }
    989         // Check that we already have joined the room before attempting to change the
    990         // nickname.
    991         if (!joined) {
    992             throw new IllegalStateException("Must be logged into the room to change nickname.");
    993         }
    994         // We change the nickname by sending a presence packet where the "to"
    995         // field is in the form "roomName@service/nickname"
    996         // We don't have to signal the MUC support again
    997         Presence joinPresence = new Presence(Presence.Type.available);
    998         joinPresence.setTo(room + "/" + nickname);
    999         // Invoke presence interceptors so that extra information can be dynamically added
   1000         for (PacketInterceptor packetInterceptor : presenceInterceptors) {
   1001             packetInterceptor.interceptPacket(joinPresence);
   1002         }
   1003 
   1004         // Wait for a presence packet back from the server.
   1005         PacketFilter responseFilter =
   1006             new AndFilter(
   1007                 new FromMatchesFilter(room + "/" + nickname),
   1008                 new PacketTypeFilter(Presence.class));
   1009         PacketCollector response = connection.createPacketCollector(responseFilter);
   1010         // Send join packet.
   1011         connection.sendPacket(joinPresence);
   1012         // Wait up to a certain number of seconds for a reply.
   1013         Presence presence =
   1014             (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1015         // Stop queuing results
   1016         response.cancel();
   1017 
   1018         if (presence == null) {
   1019             throw new XMPPException("No response from server.");
   1020         }
   1021         else if (presence.getError() != null) {
   1022             throw new XMPPException(presence.getError());
   1023         }
   1024         this.nickname = nickname;
   1025     }
   1026 
   1027     /**
   1028      * Changes the occupant's availability status within the room. The presence type
   1029      * will remain available but with a new status that describes the presence update and
   1030      * a new presence mode (e.g. Extended away).
   1031      *
   1032      * @param status a text message describing the presence update.
   1033      * @param mode the mode type for the presence update.
   1034      */
   1035     public void changeAvailabilityStatus(String status, Presence.Mode mode) {
   1036         if (nickname == null || nickname.equals("")) {
   1037             throw new IllegalArgumentException("Nickname must not be null or blank.");
   1038         }
   1039         // Check that we already have joined the room before attempting to change the
   1040         // availability status.
   1041         if (!joined) {
   1042             throw new IllegalStateException(
   1043                 "Must be logged into the room to change the " + "availability status.");
   1044         }
   1045         // We change the availability status by sending a presence packet to the room with the
   1046         // new presence status and mode
   1047         Presence joinPresence = new Presence(Presence.Type.available);
   1048         joinPresence.setStatus(status);
   1049         joinPresence.setMode(mode);
   1050         joinPresence.setTo(room + "/" + nickname);
   1051         // Invoke presence interceptors so that extra information can be dynamically added
   1052         for (PacketInterceptor packetInterceptor : presenceInterceptors) {
   1053             packetInterceptor.interceptPacket(joinPresence);
   1054         }
   1055 
   1056         // Send join packet.
   1057         connection.sendPacket(joinPresence);
   1058     }
   1059 
   1060     /**
   1061      * Kicks a visitor or participant from the room. The kicked occupant will receive a presence
   1062      * of type "unavailable" including a status code 307 and optionally along with the reason
   1063      * (if provided) and the bare JID of the user who initiated the kick. After the occupant
   1064      * was kicked from the room, the rest of the occupants will receive a presence of type
   1065      * "unavailable". The presence will include a status code 307 which means that the occupant
   1066      * was kicked from the room.
   1067      *
   1068      * @param nickname the nickname of the participant or visitor to kick from the room
   1069      * (e.g. "john").
   1070      * @param reason the reason why the participant or visitor is being kicked from the room.
   1071      * @throws XMPPException if an error occurs kicking the occupant. In particular, a
   1072      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
   1073      *      was intended to be kicked (i.e. Not Allowed error); or a
   1074      *      403 error can occur if the occupant that intended to kick another occupant does
   1075      *      not have kicking privileges (i.e. Forbidden error); or a
   1076      *      400 error can occur if the provided nickname is not present in the room.
   1077      */
   1078     public void kickParticipant(String nickname, String reason) throws XMPPException {
   1079         changeRole(nickname, "none", reason);
   1080     }
   1081 
   1082     /**
   1083      * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
   1084      * who does and does not have "voice" in the room. To have voice means that a room occupant
   1085      * is able to send messages to the room occupants.
   1086      *
   1087      * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
   1088      * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
   1089      *      403 error can occur if the occupant that intended to grant voice is not
   1090      *      a moderator in this room (i.e. Forbidden error); or a
   1091      *      400 error can occur if the provided nickname is not present in the room.
   1092      */
   1093     public void grantVoice(Collection<String> nicknames) throws XMPPException {
   1094         changeRole(nicknames, "participant");
   1095     }
   1096 
   1097     /**
   1098      * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
   1099      * who does and does not have "voice" in the room. To have voice means that a room occupant
   1100      * is able to send messages to the room occupants.
   1101      *
   1102      * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
   1103      * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
   1104      *      403 error can occur if the occupant that intended to grant voice is not
   1105      *      a moderator in this room (i.e. Forbidden error); or a
   1106      *      400 error can occur if the provided nickname is not present in the room.
   1107      */
   1108     public void grantVoice(String nickname) throws XMPPException {
   1109         changeRole(nickname, "participant", null);
   1110     }
   1111 
   1112     /**
   1113      * Revokes voice from participants in the room. In a moderated room, a moderator may want to
   1114      * revoke an occupant's privileges to speak. To have voice means that a room occupant
   1115      * is able to send messages to the room occupants.
   1116      *
   1117      * @param nicknames the nicknames of the participants to revoke voice (e.g. "john").
   1118      * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
   1119      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
   1120      *      was tried to revoke his voice (i.e. Not Allowed error); or a
   1121      *      400 error can occur if the provided nickname is not present in the room.
   1122      */
   1123     public void revokeVoice(Collection<String> nicknames) throws XMPPException {
   1124         changeRole(nicknames, "visitor");
   1125     }
   1126 
   1127     /**
   1128      * Revokes voice from a participant in the room. In a moderated room, a moderator may want to
   1129      * revoke an occupant's privileges to speak. To have voice means that a room occupant
   1130      * is able to send messages to the room occupants.
   1131      *
   1132      * @param nickname the nickname of the participant to revoke voice (e.g. "john").
   1133      * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
   1134      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
   1135      *      was tried to revoke his voice (i.e. Not Allowed error); or a
   1136      *      400 error can occur if the provided nickname is not present in the room.
   1137      */
   1138     public void revokeVoice(String nickname) throws XMPPException {
   1139         changeRole(nickname, "visitor", null);
   1140     }
   1141 
   1142     /**
   1143      * Bans users from the room. An admin or owner of the room can ban users from a room. This
   1144      * means that the banned user will no longer be able to join the room unless the ban has been
   1145      * removed. If the banned user was present in the room then he/she will be removed from the
   1146      * room and notified that he/she was banned along with the reason (if provided) and the bare
   1147      * XMPP user ID of the user who initiated the ban.
   1148      *
   1149      * @param jids the bare XMPP user IDs of the users to ban.
   1150      * @throws XMPPException if an error occurs banning a user. In particular, a
   1151      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
   1152      *      was tried to be banned (i.e. Not Allowed error).
   1153      */
   1154     public void banUsers(Collection<String> jids) throws XMPPException {
   1155         changeAffiliationByAdmin(jids, "outcast");
   1156     }
   1157 
   1158     /**
   1159      * Bans a user from the room. An admin or owner of the room can ban users from a room. This
   1160      * means that the banned user will no longer be able to join the room unless the ban has been
   1161      * removed. If the banned user was present in the room then he/she will be removed from the
   1162      * room and notified that he/she was banned along with the reason (if provided) and the bare
   1163      * XMPP user ID of the user who initiated the ban.
   1164      *
   1165      * @param jid the bare XMPP user ID of the user to ban (e.g. "user (at) host.org").
   1166      * @param reason the optional reason why the user was banned.
   1167      * @throws XMPPException if an error occurs banning a user. In particular, a
   1168      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
   1169      *      was tried to be banned (i.e. Not Allowed error).
   1170      */
   1171     public void banUser(String jid, String reason) throws XMPPException {
   1172         changeAffiliationByAdmin(jid, "outcast", reason);
   1173     }
   1174 
   1175     /**
   1176      * Grants membership to other users. Only administrators are able to grant membership. A user
   1177      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
   1178      * that a user cannot enter without being on the member list).
   1179      *
   1180      * @param jids the XMPP user IDs of the users to grant membership.
   1181      * @throws XMPPException if an error occurs granting membership to a user.
   1182      */
   1183     public void grantMembership(Collection<String> jids) throws XMPPException {
   1184         changeAffiliationByAdmin(jids, "member");
   1185     }
   1186 
   1187     /**
   1188      * Grants membership to a user. Only administrators are able to grant membership. A user
   1189      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
   1190      * that a user cannot enter without being on the member list).
   1191      *
   1192      * @param jid the XMPP user ID of the user to grant membership (e.g. "user (at) host.org").
   1193      * @throws XMPPException if an error occurs granting membership to a user.
   1194      */
   1195     public void grantMembership(String jid) throws XMPPException {
   1196         changeAffiliationByAdmin(jid, "member", null);
   1197     }
   1198 
   1199     /**
   1200      * Revokes users' membership. Only administrators are able to revoke membership. A user
   1201      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
   1202      * that a user cannot enter without being on the member list). If the user is in the room and
   1203      * the room is of type members-only then the user will be removed from the room.
   1204      *
   1205      * @param jids the bare XMPP user IDs of the users to revoke membership.
   1206      * @throws XMPPException if an error occurs revoking membership to a user.
   1207      */
   1208     public void revokeMembership(Collection<String> jids) throws XMPPException {
   1209         changeAffiliationByAdmin(jids, "none");
   1210     }
   1211 
   1212     /**
   1213      * Revokes a user's membership. Only administrators are able to revoke membership. A user
   1214      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
   1215      * that a user cannot enter without being on the member list). If the user is in the room and
   1216      * the room is of type members-only then the user will be removed from the room.
   1217      *
   1218      * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user (at) host.org").
   1219      * @throws XMPPException if an error occurs revoking membership to a user.
   1220      */
   1221     public void revokeMembership(String jid) throws XMPPException {
   1222         changeAffiliationByAdmin(jid, "none", null);
   1223     }
   1224 
   1225     /**
   1226      * Grants moderator privileges to participants or visitors. Room administrators may grant
   1227      * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
   1228      * other users, modify room's subject plus all the partcipants privileges.
   1229      *
   1230      * @param nicknames the nicknames of the occupants to grant moderator privileges.
   1231      * @throws XMPPException if an error occurs granting moderator privileges to a user.
   1232      */
   1233     public void grantModerator(Collection<String> nicknames) throws XMPPException {
   1234         changeRole(nicknames, "moderator");
   1235     }
   1236 
   1237     /**
   1238      * Grants moderator privileges to a participant or visitor. Room administrators may grant
   1239      * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
   1240      * other users, modify room's subject plus all the partcipants privileges.
   1241      *
   1242      * @param nickname the nickname of the occupant to grant moderator privileges.
   1243      * @throws XMPPException if an error occurs granting moderator privileges to a user.
   1244      */
   1245     public void grantModerator(String nickname) throws XMPPException {
   1246         changeRole(nickname, "moderator", null);
   1247     }
   1248 
   1249     /**
   1250      * Revokes moderator privileges from other users. The occupant that loses moderator
   1251      * privileges will become a participant. Room administrators may revoke moderator privileges
   1252      * only to occupants whose affiliation is member or none. This means that an administrator is
   1253      * not allowed to revoke moderator privileges from other room administrators or owners.
   1254      *
   1255      * @param nicknames the nicknames of the occupants to revoke moderator privileges.
   1256      * @throws XMPPException if an error occurs revoking moderator privileges from a user.
   1257      */
   1258     public void revokeModerator(Collection<String> nicknames) throws XMPPException {
   1259         changeRole(nicknames, "participant");
   1260     }
   1261 
   1262     /**
   1263      * Revokes moderator privileges from another user. The occupant that loses moderator
   1264      * privileges will become a participant. Room administrators may revoke moderator privileges
   1265      * only to occupants whose affiliation is member or none. This means that an administrator is
   1266      * not allowed to revoke moderator privileges from other room administrators or owners.
   1267      *
   1268      * @param nickname the nickname of the occupant to revoke moderator privileges.
   1269      * @throws XMPPException if an error occurs revoking moderator privileges from a user.
   1270      */
   1271     public void revokeModerator(String nickname) throws XMPPException {
   1272         changeRole(nickname, "participant", null);
   1273     }
   1274 
   1275     /**
   1276      * Grants ownership privileges to other users. Room owners may grant ownership privileges.
   1277      * Some room implementations will not allow to grant ownership privileges to other users.
   1278      * An owner is allowed to change defining room features as well as perform all administrative
   1279      * functions.
   1280      *
   1281      * @param jids the collection of bare XMPP user IDs of the users to grant ownership.
   1282      * @throws XMPPException if an error occurs granting ownership privileges to a user.
   1283      */
   1284     public void grantOwnership(Collection<String> jids) throws XMPPException {
   1285         changeAffiliationByAdmin(jids, "owner");
   1286     }
   1287 
   1288     /**
   1289      * Grants ownership privileges to another user. Room owners may grant ownership privileges.
   1290      * Some room implementations will not allow to grant ownership privileges to other users.
   1291      * An owner is allowed to change defining room features as well as perform all administrative
   1292      * functions.
   1293      *
   1294      * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user (at) host.org").
   1295      * @throws XMPPException if an error occurs granting ownership privileges to a user.
   1296      */
   1297     public void grantOwnership(String jid) throws XMPPException {
   1298         changeAffiliationByAdmin(jid, "owner", null);
   1299     }
   1300 
   1301     /**
   1302      * Revokes ownership privileges from other users. The occupant that loses ownership
   1303      * privileges will become an administrator. Room owners may revoke ownership privileges.
   1304      * Some room implementations will not allow to grant ownership privileges to other users.
   1305      *
   1306      * @param jids the bare XMPP user IDs of the users to revoke ownership.
   1307      * @throws XMPPException if an error occurs revoking ownership privileges from a user.
   1308      */
   1309     public void revokeOwnership(Collection<String> jids) throws XMPPException {
   1310         changeAffiliationByAdmin(jids, "admin");
   1311     }
   1312 
   1313     /**
   1314      * Revokes ownership privileges from another user. The occupant that loses ownership
   1315      * privileges will become an administrator. Room owners may revoke ownership privileges.
   1316      * Some room implementations will not allow to grant ownership privileges to other users.
   1317      *
   1318      * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user (at) host.org").
   1319      * @throws XMPPException if an error occurs revoking ownership privileges from a user.
   1320      */
   1321     public void revokeOwnership(String jid) throws XMPPException {
   1322         changeAffiliationByAdmin(jid, "admin", null);
   1323     }
   1324 
   1325     /**
   1326      * Grants administrator privileges to other users. Room owners may grant administrator
   1327      * privileges to a member or unaffiliated user. An administrator is allowed to perform
   1328      * administrative functions such as banning users and edit moderator list.
   1329      *
   1330      * @param jids the bare XMPP user IDs of the users to grant administrator privileges.
   1331      * @throws XMPPException if an error occurs granting administrator privileges to a user.
   1332      */
   1333     public void grantAdmin(Collection<String> jids) throws XMPPException {
   1334         changeAffiliationByOwner(jids, "admin");
   1335     }
   1336 
   1337     /**
   1338      * Grants administrator privileges to another user. Room owners may grant administrator
   1339      * privileges to a member or unaffiliated user. An administrator is allowed to perform
   1340      * administrative functions such as banning users and edit moderator list.
   1341      *
   1342      * @param jid the bare XMPP user ID of the user to grant administrator privileges
   1343      * (e.g. "user (at) host.org").
   1344      * @throws XMPPException if an error occurs granting administrator privileges to a user.
   1345      */
   1346     public void grantAdmin(String jid) throws XMPPException {
   1347         changeAffiliationByOwner(jid, "admin");
   1348     }
   1349 
   1350     /**
   1351      * Revokes administrator privileges from users. The occupant that loses administrator
   1352      * privileges will become a member. Room owners may revoke administrator privileges from
   1353      * a member or unaffiliated user.
   1354      *
   1355      * @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
   1356      * @throws XMPPException if an error occurs revoking administrator privileges from a user.
   1357      */
   1358     public void revokeAdmin(Collection<String> jids) throws XMPPException {
   1359         changeAffiliationByOwner(jids, "member");
   1360     }
   1361 
   1362     /**
   1363      * Revokes administrator privileges from a user. The occupant that loses administrator
   1364      * privileges will become a member. Room owners may revoke administrator privileges from
   1365      * a member or unaffiliated user.
   1366      *
   1367      * @param jid the bare XMPP user ID of the user to revoke administrator privileges
   1368      * (e.g. "user (at) host.org").
   1369      * @throws XMPPException if an error occurs revoking administrator privileges from a user.
   1370      */
   1371     public void revokeAdmin(String jid) throws XMPPException {
   1372         changeAffiliationByOwner(jid, "member");
   1373     }
   1374 
   1375     private void changeAffiliationByOwner(String jid, String affiliation) throws XMPPException {
   1376         MUCOwner iq = new MUCOwner();
   1377         iq.setTo(room);
   1378         iq.setType(IQ.Type.SET);
   1379         // Set the new affiliation.
   1380         MUCOwner.Item item = new MUCOwner.Item(affiliation);
   1381         item.setJid(jid);
   1382         iq.addItem(item);
   1383 
   1384         // Wait for a response packet back from the server.
   1385         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
   1386         PacketCollector response = connection.createPacketCollector(responseFilter);
   1387         // Send the change request to the server.
   1388         connection.sendPacket(iq);
   1389         // Wait up to a certain number of seconds for a reply.
   1390         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1391         // Stop queuing results
   1392         response.cancel();
   1393 
   1394         if (answer == null) {
   1395             throw new XMPPException("No response from server.");
   1396         }
   1397         else if (answer.getError() != null) {
   1398             throw new XMPPException(answer.getError());
   1399         }
   1400     }
   1401 
   1402     private void changeAffiliationByOwner(Collection<String> jids, String affiliation)
   1403             throws XMPPException {
   1404         MUCOwner iq = new MUCOwner();
   1405         iq.setTo(room);
   1406         iq.setType(IQ.Type.SET);
   1407         for (String jid : jids) {
   1408             // Set the new affiliation.
   1409             MUCOwner.Item item = new MUCOwner.Item(affiliation);
   1410             item.setJid(jid);
   1411             iq.addItem(item);
   1412         }
   1413 
   1414         // Wait for a response packet back from the server.
   1415         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
   1416         PacketCollector response = connection.createPacketCollector(responseFilter);
   1417         // Send the change request to the server.
   1418         connection.sendPacket(iq);
   1419         // Wait up to a certain number of seconds for a reply.
   1420         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1421         // Stop queuing results
   1422         response.cancel();
   1423 
   1424         if (answer == null) {
   1425             throw new XMPPException("No response from server.");
   1426         }
   1427         else if (answer.getError() != null) {
   1428             throw new XMPPException(answer.getError());
   1429         }
   1430     }
   1431 
   1432     /**
   1433      * Tries to change the affiliation with an 'muc#admin' namespace
   1434      *
   1435      * @param jid
   1436      * @param affiliation
   1437      * @param reason the reason for the affiliation change (optional)
   1438      * @throws XMPPException
   1439      */
   1440     private void changeAffiliationByAdmin(String jid, String affiliation, String reason)
   1441             throws XMPPException {
   1442         MUCAdmin iq = new MUCAdmin();
   1443         iq.setTo(room);
   1444         iq.setType(IQ.Type.SET);
   1445         // Set the new affiliation.
   1446         MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
   1447         item.setJid(jid);
   1448         if(reason != null)
   1449             item.setReason(reason);
   1450         iq.addItem(item);
   1451 
   1452         // Wait for a response packet back from the server.
   1453         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
   1454         PacketCollector response = connection.createPacketCollector(responseFilter);
   1455         // Send the change request to the server.
   1456         connection.sendPacket(iq);
   1457         // Wait up to a certain number of seconds for a reply.
   1458         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1459         // Stop queuing results
   1460         response.cancel();
   1461 
   1462         if (answer == null) {
   1463             throw new XMPPException("No response from server.");
   1464         }
   1465         else if (answer.getError() != null) {
   1466             throw new XMPPException(answer.getError());
   1467         }
   1468     }
   1469 
   1470     private void changeAffiliationByAdmin(Collection<String> jids, String affiliation)
   1471             throws XMPPException {
   1472         MUCAdmin iq = new MUCAdmin();
   1473         iq.setTo(room);
   1474         iq.setType(IQ.Type.SET);
   1475         for (String jid : jids) {
   1476             // Set the new affiliation.
   1477             MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
   1478             item.setJid(jid);
   1479             iq.addItem(item);
   1480         }
   1481 
   1482         // Wait for a response packet back from the server.
   1483         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
   1484         PacketCollector response = connection.createPacketCollector(responseFilter);
   1485         // Send the change request to the server.
   1486         connection.sendPacket(iq);
   1487         // Wait up to a certain number of seconds for a reply.
   1488         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1489         // Stop queuing results
   1490         response.cancel();
   1491 
   1492         if (answer == null) {
   1493             throw new XMPPException("No response from server.");
   1494         }
   1495         else if (answer.getError() != null) {
   1496             throw new XMPPException(answer.getError());
   1497         }
   1498     }
   1499 
   1500     private void changeRole(String nickname, String role, String reason) throws XMPPException {
   1501         MUCAdmin iq = new MUCAdmin();
   1502         iq.setTo(room);
   1503         iq.setType(IQ.Type.SET);
   1504         // Set the new role.
   1505         MUCAdmin.Item item = new MUCAdmin.Item(null, role);
   1506         item.setNick(nickname);
   1507         item.setReason(reason);
   1508         iq.addItem(item);
   1509 
   1510         // Wait for a response packet back from the server.
   1511         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
   1512         PacketCollector response = connection.createPacketCollector(responseFilter);
   1513         // Send the change request to the server.
   1514         connection.sendPacket(iq);
   1515         // Wait up to a certain number of seconds for a reply.
   1516         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1517         // Stop queuing results
   1518         response.cancel();
   1519 
   1520         if (answer == null) {
   1521             throw new XMPPException("No response from server.");
   1522         }
   1523         else if (answer.getError() != null) {
   1524             throw new XMPPException(answer.getError());
   1525         }
   1526     }
   1527 
   1528     private void changeRole(Collection<String> nicknames, String role) throws XMPPException {
   1529         MUCAdmin iq = new MUCAdmin();
   1530         iq.setTo(room);
   1531         iq.setType(IQ.Type.SET);
   1532         for (String nickname : nicknames) {
   1533             // Set the new role.
   1534             MUCAdmin.Item item = new MUCAdmin.Item(null, role);
   1535             item.setNick(nickname);
   1536             iq.addItem(item);
   1537         }
   1538 
   1539         // Wait for a response packet back from the server.
   1540         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
   1541         PacketCollector response = connection.createPacketCollector(responseFilter);
   1542         // Send the change request to the server.
   1543         connection.sendPacket(iq);
   1544         // Wait up to a certain number of seconds for a reply.
   1545         IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1546         // Stop queuing results
   1547         response.cancel();
   1548 
   1549         if (answer == null) {
   1550             throw new XMPPException("No response from server.");
   1551         }
   1552         else if (answer.getError() != null) {
   1553             throw new XMPPException(answer.getError());
   1554         }
   1555     }
   1556 
   1557     /**
   1558      * Returns the number of occupants in the group chat.<p>
   1559      *
   1560      * Note: this value will only be accurate after joining the group chat, and
   1561      * may fluctuate over time. If you query this value directly after joining the
   1562      * group chat it may not be accurate, as it takes a certain amount of time for
   1563      * the server to send all presence packets to this client.
   1564      *
   1565      * @return the number of occupants in the group chat.
   1566      */
   1567     public int getOccupantsCount() {
   1568         return occupantsMap.size();
   1569     }
   1570 
   1571     /**
   1572      * Returns an Iterator (of Strings) for the list of fully qualified occupants
   1573      * in the group chat. For example, "conference (at) chat.jivesoftware.com/SomeUser".
   1574      * Typically, a client would only display the nickname of the occupant. To
   1575      * get the nickname from the fully qualified name, use the
   1576      * {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method.
   1577      * Note: this value will only be accurate after joining the group chat, and may
   1578      * fluctuate over time.
   1579      *
   1580      * @return an Iterator for the occupants in the group chat.
   1581      */
   1582     public Iterator<String> getOccupants() {
   1583         return Collections.unmodifiableList(new ArrayList<String>(occupantsMap.keySet()))
   1584                 .iterator();
   1585     }
   1586 
   1587     /**
   1588      * Returns the presence info for a particular user, or <tt>null</tt> if the user
   1589      * is not in the room.<p>
   1590      *
   1591      * @param user the room occupant to search for his presence. The format of user must
   1592      * be: roomName@service/nickname (e.g. darkcave (at) macbeth.shakespeare.lit/thirdwitch).
   1593      * @return the occupant's current presence, or <tt>null</tt> if the user is unavailable
   1594      *      or if no presence information is available.
   1595      */
   1596     public Presence getOccupantPresence(String user) {
   1597         return occupantsMap.get(user);
   1598     }
   1599 
   1600     /**
   1601      * Returns the Occupant information for a particular occupant, or <tt>null</tt> if the
   1602      * user is not in the room. The Occupant object may include information such as full
   1603      * JID of the user as well as the role and affiliation of the user in the room.<p>
   1604      *
   1605      * @param user the room occupant to search for his presence. The format of user must
   1606      * be: roomName@service/nickname (e.g. darkcave (at) macbeth.shakespeare.lit/thirdwitch).
   1607      * @return the Occupant or <tt>null</tt> if the user is unavailable (i.e. not in the room).
   1608      */
   1609     public Occupant getOccupant(String user) {
   1610         Presence presence = occupantsMap.get(user);
   1611         if (presence != null) {
   1612             return new Occupant(presence);
   1613         }
   1614         return null;
   1615     }
   1616 
   1617     /**
   1618      * Adds a packet listener that will be notified of any new Presence packets
   1619      * sent to the group chat. Using a listener is a suitable way to know when the list
   1620      * of occupants should be re-loaded due to any changes.
   1621      *
   1622      * @param listener a packet listener that will be notified of any presence packets
   1623      *      sent to the group chat.
   1624      */
   1625     public void addParticipantListener(PacketListener listener) {
   1626         connection.addPacketListener(listener, presenceFilter);
   1627         connectionListeners.add(listener);
   1628     }
   1629 
   1630     /**
   1631      * Remoces a packet listener that was being notified of any new Presence packets
   1632      * sent to the group chat.
   1633      *
   1634      * @param listener a packet listener that was being notified of any presence packets
   1635      *      sent to the group chat.
   1636      */
   1637     public void removeParticipantListener(PacketListener listener) {
   1638         connection.removePacketListener(listener);
   1639         connectionListeners.remove(listener);
   1640     }
   1641 
   1642     /**
   1643      * Returns a collection of <code>Affiliate</code> with the room owners.
   1644      *
   1645      * @return a collection of <code>Affiliate</code> with the room owners.
   1646      * @throws XMPPException if an error occured while performing the request to the server or you
   1647      *         don't have enough privileges to get this information.
   1648      */
   1649     public Collection<Affiliate> getOwners() throws XMPPException {
   1650         return getAffiliatesByAdmin("owner");
   1651     }
   1652 
   1653     /**
   1654      * Returns a collection of <code>Affiliate</code> with the room administrators.
   1655      *
   1656      * @return a collection of <code>Affiliate</code> with the room administrators.
   1657      * @throws XMPPException if an error occured while performing the request to the server or you
   1658      *         don't have enough privileges to get this information.
   1659      */
   1660     public Collection<Affiliate> getAdmins() throws XMPPException {
   1661         return getAffiliatesByOwner("admin");
   1662     }
   1663 
   1664     /**
   1665      * Returns a collection of <code>Affiliate</code> with the room members.
   1666      *
   1667      * @return a collection of <code>Affiliate</code> with the room members.
   1668      * @throws XMPPException if an error occured while performing the request to the server or you
   1669      *         don't have enough privileges to get this information.
   1670      */
   1671     public Collection<Affiliate> getMembers() throws XMPPException {
   1672         return getAffiliatesByAdmin("member");
   1673     }
   1674 
   1675     /**
   1676      * Returns a collection of <code>Affiliate</code> with the room outcasts.
   1677      *
   1678      * @return a collection of <code>Affiliate</code> with the room outcasts.
   1679      * @throws XMPPException if an error occured while performing the request to the server or you
   1680      *         don't have enough privileges to get this information.
   1681      */
   1682     public Collection<Affiliate> getOutcasts() throws XMPPException {
   1683         return getAffiliatesByAdmin("outcast");
   1684     }
   1685 
   1686     /**
   1687      * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
   1688      * sending a request in the owner namespace.
   1689      *
   1690      * @param affiliation the affiliation of the users in the room.
   1691      * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
   1692      * @throws XMPPException if an error occured while performing the request to the server or you
   1693      *         don't have enough privileges to get this information.
   1694      */
   1695     private Collection<Affiliate> getAffiliatesByOwner(String affiliation) throws XMPPException {
   1696         MUCOwner iq = new MUCOwner();
   1697         iq.setTo(room);
   1698         iq.setType(IQ.Type.GET);
   1699         // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
   1700         MUCOwner.Item item = new MUCOwner.Item(affiliation);
   1701         iq.addItem(item);
   1702 
   1703         // Wait for a response packet back from the server.
   1704         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
   1705         PacketCollector response = connection.createPacketCollector(responseFilter);
   1706         // Send the request to the server.
   1707         connection.sendPacket(iq);
   1708         // Wait up to a certain number of seconds for a reply.
   1709         MUCOwner answer = (MUCOwner) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1710         // Stop queuing results
   1711         response.cancel();
   1712 
   1713         if (answer == null) {
   1714             throw new XMPPException("No response from server.");
   1715         }
   1716         else if (answer.getError() != null) {
   1717             throw new XMPPException(answer.getError());
   1718         }
   1719         // Get the list of affiliates from the server's answer
   1720         List<Affiliate> affiliates = new ArrayList<Affiliate>();
   1721         for (Iterator<MUCOwner.Item> it = answer.getItems(); it.hasNext();) {
   1722             affiliates.add(new Affiliate(it.next()));
   1723         }
   1724         return affiliates;
   1725     }
   1726 
   1727     /**
   1728      * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
   1729      * sending a request in the admin namespace.
   1730      *
   1731      * @param affiliation the affiliation of the users in the room.
   1732      * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
   1733      * @throws XMPPException if an error occured while performing the request to the server or you
   1734      *         don't have enough privileges to get this information.
   1735      */
   1736     private Collection<Affiliate> getAffiliatesByAdmin(String affiliation) throws XMPPException {
   1737         MUCAdmin iq = new MUCAdmin();
   1738         iq.setTo(room);
   1739         iq.setType(IQ.Type.GET);
   1740         // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
   1741         MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
   1742         iq.addItem(item);
   1743 
   1744         // Wait for a response packet back from the server.
   1745         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
   1746         PacketCollector response = connection.createPacketCollector(responseFilter);
   1747         // Send the request to the server.
   1748         connection.sendPacket(iq);
   1749         // Wait up to a certain number of seconds for a reply.
   1750         MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1751         // Stop queuing results
   1752         response.cancel();
   1753 
   1754         if (answer == null) {
   1755             throw new XMPPException("No response from server.");
   1756         }
   1757         else if (answer.getError() != null) {
   1758             throw new XMPPException(answer.getError());
   1759         }
   1760         // Get the list of affiliates from the server's answer
   1761         List<Affiliate> affiliates = new ArrayList<Affiliate>();
   1762         for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) {
   1763             affiliates.add(new Affiliate(it.next()));
   1764         }
   1765         return affiliates;
   1766     }
   1767 
   1768     /**
   1769      * Returns a collection of <code>Occupant</code> with the room moderators.
   1770      *
   1771      * @return a collection of <code>Occupant</code> with the room moderators.
   1772      * @throws XMPPException if an error occured while performing the request to the server or you
   1773      *         don't have enough privileges to get this information.
   1774      */
   1775     public Collection<Occupant> getModerators() throws XMPPException {
   1776         return getOccupants("moderator");
   1777     }
   1778 
   1779     /**
   1780      * Returns a collection of <code>Occupant</code> with the room participants.
   1781      *
   1782      * @return a collection of <code>Occupant</code> with the room participants.
   1783      * @throws XMPPException if an error occured while performing the request to the server or you
   1784      *         don't have enough privileges to get this information.
   1785      */
   1786     public Collection<Occupant> getParticipants() throws XMPPException {
   1787         return getOccupants("participant");
   1788     }
   1789 
   1790     /**
   1791      * Returns a collection of <code>Occupant</code> that have the specified room role.
   1792      *
   1793      * @param role the role of the occupant in the room.
   1794      * @return a collection of <code>Occupant</code> that have the specified room role.
   1795      * @throws XMPPException if an error occured while performing the request to the server or you
   1796      *         don't have enough privileges to get this information.
   1797      */
   1798     private Collection<Occupant> getOccupants(String role) throws XMPPException {
   1799         MUCAdmin iq = new MUCAdmin();
   1800         iq.setTo(room);
   1801         iq.setType(IQ.Type.GET);
   1802         // Set the specified role. This may request the list of moderators/participants.
   1803         MUCAdmin.Item item = new MUCAdmin.Item(null, role);
   1804         iq.addItem(item);
   1805 
   1806         // Wait for a response packet back from the server.
   1807         PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
   1808         PacketCollector response = connection.createPacketCollector(responseFilter);
   1809         // Send the request to the server.
   1810         connection.sendPacket(iq);
   1811         // Wait up to a certain number of seconds for a reply.
   1812         MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1813         // Stop queuing results
   1814         response.cancel();
   1815 
   1816         if (answer == null) {
   1817             throw new XMPPException("No response from server.");
   1818         }
   1819         else if (answer.getError() != null) {
   1820             throw new XMPPException(answer.getError());
   1821         }
   1822         // Get the list of participants from the server's answer
   1823         List<Occupant> participants = new ArrayList<Occupant>();
   1824         for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) {
   1825             participants.add(new Occupant(it.next()));
   1826         }
   1827         return participants;
   1828     }
   1829 
   1830     /**
   1831      * Sends a message to the chat room.
   1832      *
   1833      * @param text the text of the message to send.
   1834      * @throws XMPPException if sending the message fails.
   1835      */
   1836     public void sendMessage(String text) throws XMPPException {
   1837         Message message = new Message(room, Message.Type.groupchat);
   1838         message.setBody(text);
   1839         connection.sendPacket(message);
   1840     }
   1841 
   1842     /**
   1843      * Returns a new Chat for sending private messages to a given room occupant.
   1844      * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
   1845      * service will change the 'from' address to the sender's room JID and delivering the message
   1846      * to the intended recipient's full JID.
   1847      *
   1848      * @param occupant occupant unique room JID (e.g. 'darkcave (at) macbeth.shakespeare.lit/Paul').
   1849      * @param listener the listener is a message listener that will handle messages for the newly
   1850      * created chat.
   1851      * @return new Chat for sending private messages to a given room occupant.
   1852      */
   1853     public Chat createPrivateChat(String occupant, MessageListener listener) {
   1854         return connection.getChatManager().createChat(occupant, listener);
   1855     }
   1856 
   1857     /**
   1858      * Creates a new Message to send to the chat room.
   1859      *
   1860      * @return a new Message addressed to the chat room.
   1861      */
   1862     public Message createMessage() {
   1863         return new Message(room, Message.Type.groupchat);
   1864     }
   1865 
   1866     /**
   1867      * Sends a Message to the chat room.
   1868      *
   1869      * @param message the message.
   1870      * @throws XMPPException if sending the message fails.
   1871      */
   1872     public void sendMessage(Message message) throws XMPPException {
   1873         connection.sendPacket(message);
   1874     }
   1875 
   1876     /**
   1877     * Polls for and returns the next message, or <tt>null</tt> if there isn't
   1878     * a message immediately available. This method provides significantly different
   1879     * functionalty than the {@link #nextMessage()} method since it's non-blocking.
   1880     * In other words, the method call will always return immediately, whereas the
   1881     * nextMessage method will return only when a message is available (or after
   1882     * a specific timeout).
   1883     *
   1884     * @return the next message if one is immediately available and
   1885     *      <tt>null</tt> otherwise.
   1886     */
   1887     public Message pollMessage() {
   1888         return (Message) messageCollector.pollResult();
   1889     }
   1890 
   1891     /**
   1892      * Returns the next available message in the chat. The method call will block
   1893      * (not return) until a message is available.
   1894      *
   1895      * @return the next message.
   1896      */
   1897     public Message nextMessage() {
   1898         return (Message) messageCollector.nextResult();
   1899     }
   1900 
   1901     /**
   1902      * Returns the next available message in the chat. The method call will block
   1903      * (not return) until a packet is available or the <tt>timeout</tt> has elapased.
   1904      * If the timeout elapses without a result, <tt>null</tt> will be returned.
   1905      *
   1906      * @param timeout the maximum amount of time to wait for the next message.
   1907      * @return the next message, or <tt>null</tt> if the timeout elapses without a
   1908      *      message becoming available.
   1909      */
   1910     public Message nextMessage(long timeout) {
   1911         return (Message) messageCollector.nextResult(timeout);
   1912     }
   1913 
   1914     /**
   1915      * Adds a packet listener that will be notified of any new messages in the
   1916      * group chat. Only "group chat" messages addressed to this group chat will
   1917      * be delivered to the listener. If you wish to listen for other packets
   1918      * that may be associated with this group chat, you should register a
   1919      * PacketListener directly with the Connection with the appropriate
   1920      * PacketListener.
   1921      *
   1922      * @param listener a packet listener.
   1923      */
   1924     public void addMessageListener(PacketListener listener) {
   1925         connection.addPacketListener(listener, messageFilter);
   1926         connectionListeners.add(listener);
   1927     }
   1928 
   1929     /**
   1930      * Removes a packet listener that was being notified of any new messages in the
   1931      * multi user chat. Only "group chat" messages addressed to this multi user chat were
   1932      * being delivered to the listener.
   1933      *
   1934      * @param listener a packet listener.
   1935      */
   1936     public void removeMessageListener(PacketListener listener) {
   1937         connection.removePacketListener(listener);
   1938         connectionListeners.remove(listener);
   1939     }
   1940 
   1941     /**
   1942      * Changes the subject within the room. As a default, only users with a role of "moderator"
   1943      * are allowed to change the subject in a room. Although some rooms may be configured to
   1944      * allow a mere participant or even a visitor to change the subject.
   1945      *
   1946      * @param subject the new room's subject to set.
   1947      * @throws XMPPException if someone without appropriate privileges attempts to change the
   1948      *          room subject will throw an error with code 403 (i.e. Forbidden)
   1949      */
   1950     public void changeSubject(final String subject) throws XMPPException {
   1951         Message message = new Message(room, Message.Type.groupchat);
   1952         message.setSubject(subject);
   1953         // Wait for an error or confirmation message back from the server.
   1954         PacketFilter responseFilter =
   1955             new AndFilter(
   1956                 new FromMatchesFilter(room),
   1957                 new PacketTypeFilter(Message.class));
   1958         responseFilter = new AndFilter(responseFilter, new PacketFilter() {
   1959             public boolean accept(Packet packet) {
   1960                 Message msg = (Message) packet;
   1961                 return subject.equals(msg.getSubject());
   1962             }
   1963         });
   1964         PacketCollector response = connection.createPacketCollector(responseFilter);
   1965         // Send change subject packet.
   1966         connection.sendPacket(message);
   1967         // Wait up to a certain number of seconds for a reply.
   1968         Message answer =
   1969             (Message) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
   1970         // Stop queuing results
   1971         response.cancel();
   1972 
   1973         if (answer == null) {
   1974             throw new XMPPException("No response from server.");
   1975         }
   1976         else if (answer.getError() != null) {
   1977             throw new XMPPException(answer.getError());
   1978         }
   1979     }
   1980 
   1981     /**
   1982      * Notification message that the user has joined the room.
   1983      */
   1984     private synchronized void userHasJoined() {
   1985         // Update the list of joined rooms through this connection
   1986         List<String> rooms = joinedRooms.get(connection);
   1987         if (rooms == null) {
   1988             rooms = new ArrayList<String>();
   1989             joinedRooms.put(connection, rooms);
   1990         }
   1991         rooms.add(room);
   1992     }
   1993 
   1994     /**
   1995      * Notification message that the user has left the room.
   1996      */
   1997     private synchronized void userHasLeft() {
   1998         // Update the list of joined rooms through this connection
   1999         List<String> rooms = joinedRooms.get(connection);
   2000         if (rooms == null) {
   2001             return;
   2002         }
   2003         rooms.remove(room);
   2004         cleanup();
   2005     }
   2006 
   2007     /**
   2008      * Returns the MUCUser packet extension included in the packet or <tt>null</tt> if none.
   2009      *
   2010      * @param packet the packet that may include the MUCUser extension.
   2011      * @return the MUCUser found in the packet.
   2012      */
   2013     private MUCUser getMUCUserExtension(Packet packet) {
   2014         if (packet != null) {
   2015             // Get the MUC User extension
   2016             return (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
   2017         }
   2018         return null;
   2019     }
   2020 
   2021     /**
   2022      * Adds a listener that will be notified of changes in your status in the room
   2023      * such as the user being kicked, banned, or granted admin permissions.
   2024      *
   2025      * @param listener a user status listener.
   2026      */
   2027     public void addUserStatusListener(UserStatusListener listener) {
   2028         synchronized (userStatusListeners) {
   2029             if (!userStatusListeners.contains(listener)) {
   2030                 userStatusListeners.add(listener);
   2031             }
   2032         }
   2033     }
   2034 
   2035     /**
   2036      * Removes a listener that was being notified of changes in your status in the room
   2037      * such as the user being kicked, banned, or granted admin permissions.
   2038      *
   2039      * @param listener a user status listener.
   2040      */
   2041     public void removeUserStatusListener(UserStatusListener listener) {
   2042         synchronized (userStatusListeners) {
   2043             userStatusListeners.remove(listener);
   2044         }
   2045     }
   2046 
   2047     private void fireUserStatusListeners(String methodName, Object[] params) {
   2048         UserStatusListener[] listeners;
   2049         synchronized (userStatusListeners) {
   2050             listeners = new UserStatusListener[userStatusListeners.size()];
   2051             userStatusListeners.toArray(listeners);
   2052         }
   2053         // Get the classes of the method parameters
   2054         Class<?>[] paramClasses = new Class[params.length];
   2055         for (int i = 0; i < params.length; i++) {
   2056             paramClasses[i] = params[i].getClass();
   2057         }
   2058         try {
   2059             // Get the method to execute based on the requested methodName and parameters classes
   2060             Method method = UserStatusListener.class.getDeclaredMethod(methodName, paramClasses);
   2061             for (UserStatusListener listener : listeners) {
   2062                 method.invoke(listener, params);
   2063             }
   2064         } catch (NoSuchMethodException e) {
   2065             e.printStackTrace();
   2066         } catch (InvocationTargetException e) {
   2067             e.printStackTrace();
   2068         } catch (IllegalAccessException e) {
   2069             e.printStackTrace();
   2070         }
   2071     }
   2072 
   2073     /**
   2074      * Adds a listener that will be notified of changes in occupants status in the room
   2075      * such as the user being kicked, banned, or granted admin permissions.
   2076      *
   2077      * @param listener a participant status listener.
   2078      */
   2079     public void addParticipantStatusListener(ParticipantStatusListener listener) {
   2080         synchronized (participantStatusListeners) {
   2081             if (!participantStatusListeners.contains(listener)) {
   2082                 participantStatusListeners.add(listener);
   2083             }
   2084         }
   2085     }
   2086 
   2087     /**
   2088      * Removes a listener that was being notified of changes in occupants status in the room
   2089      * such as the user being kicked, banned, or granted admin permissions.
   2090      *
   2091      * @param listener a participant status listener.
   2092      */
   2093     public void removeParticipantStatusListener(ParticipantStatusListener listener) {
   2094         synchronized (participantStatusListeners) {
   2095             participantStatusListeners.remove(listener);
   2096         }
   2097     }
   2098 
   2099     private void fireParticipantStatusListeners(String methodName, List<String> params) {
   2100         ParticipantStatusListener[] listeners;
   2101         synchronized (participantStatusListeners) {
   2102             listeners = new ParticipantStatusListener[participantStatusListeners.size()];
   2103             participantStatusListeners.toArray(listeners);
   2104         }
   2105         try {
   2106             // Get the method to execute based on the requested methodName and parameter
   2107             Class<?>[] classes = new Class[params.size()];
   2108             for (int i=0;i<params.size(); i++) {
   2109                 classes[i] = String.class;
   2110             }
   2111             Method method = ParticipantStatusListener.class.getDeclaredMethod(methodName, classes);
   2112             for (ParticipantStatusListener listener : listeners) {
   2113                 method.invoke(listener, params.toArray());
   2114             }
   2115         } catch (NoSuchMethodException e) {
   2116             e.printStackTrace();
   2117         } catch (InvocationTargetException e) {
   2118             e.printStackTrace();
   2119         } catch (IllegalAccessException e) {
   2120             e.printStackTrace();
   2121         }
   2122     }
   2123 
   2124     private void init() {
   2125         // Create filters
   2126         messageFilter =
   2127             new AndFilter(
   2128                 new FromMatchesFilter(room),
   2129                 new MessageTypeFilter(Message.Type.groupchat));
   2130         messageFilter = new AndFilter(messageFilter, new PacketFilter() {
   2131             public boolean accept(Packet packet) {
   2132                 Message msg = (Message) packet;
   2133                 return msg.getBody() != null;
   2134             }
   2135         });
   2136         presenceFilter =
   2137             new AndFilter(new FromMatchesFilter(room), new PacketTypeFilter(Presence.class));
   2138 
   2139         // Create a collector for incoming messages.
   2140         messageCollector = new ConnectionDetachedPacketCollector();
   2141 
   2142         // Create a listener for subject updates.
   2143         PacketListener subjectListener = new PacketListener() {
   2144             public void processPacket(Packet packet) {
   2145                 Message msg = (Message) packet;
   2146                 // Update the room subject
   2147                 subject = msg.getSubject();
   2148                 // Fire event for subject updated listeners
   2149                 fireSubjectUpdatedListeners(
   2150                     msg.getSubject(),
   2151                     msg.getFrom());
   2152 
   2153             }
   2154         };
   2155 
   2156         // Create a listener for all presence updates.
   2157         PacketListener presenceListener = new PacketListener() {
   2158             public void processPacket(Packet packet) {
   2159                 Presence presence = (Presence) packet;
   2160                 String from = presence.getFrom();
   2161                 String myRoomJID = room + "/" + nickname;
   2162                 boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
   2163                 if (presence.getType() == Presence.Type.available) {
   2164                     Presence oldPresence = occupantsMap.put(from, presence);
   2165                     if (oldPresence != null) {
   2166                         // Get the previous occupant's affiliation & role
   2167                         MUCUser mucExtension = getMUCUserExtension(oldPresence);
   2168                         String oldAffiliation = mucExtension.getItem().getAffiliation();
   2169                         String oldRole = mucExtension.getItem().getRole();
   2170                         // Get the new occupant's affiliation & role
   2171                         mucExtension = getMUCUserExtension(presence);
   2172                         String newAffiliation = mucExtension.getItem().getAffiliation();
   2173                         String newRole = mucExtension.getItem().getRole();
   2174                         // Fire role modification events
   2175                         checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
   2176                         // Fire affiliation modification events
   2177                         checkAffiliationModifications(
   2178                             oldAffiliation,
   2179                             newAffiliation,
   2180                             isUserStatusModification,
   2181                             from);
   2182                     }
   2183                     else {
   2184                         // A new occupant has joined the room
   2185                         if (!isUserStatusModification) {
   2186                             List<String> params = new ArrayList<String>();
   2187                             params.add(from);
   2188                             fireParticipantStatusListeners("joined", params);
   2189                         }
   2190                     }
   2191                 }
   2192                 else if (presence.getType() == Presence.Type.unavailable) {
   2193                     occupantsMap.remove(from);
   2194                     MUCUser mucUser = getMUCUserExtension(presence);
   2195                     if (mucUser != null && mucUser.getStatus() != null) {
   2196                         // Fire events according to the received presence code
   2197                         checkPresenceCode(
   2198                             mucUser.getStatus().getCode(),
   2199                             presence.getFrom().equals(myRoomJID),
   2200                             mucUser,
   2201                             from);
   2202                     } else {
   2203                         // An occupant has left the room
   2204                         if (!isUserStatusModification) {
   2205                             List<String> params = new ArrayList<String>();
   2206                             params.add(from);
   2207                             fireParticipantStatusListeners("left", params);
   2208                         }
   2209                     }
   2210                 }
   2211             }
   2212         };
   2213 
   2214         // Listens for all messages that include a MUCUser extension and fire the invitation
   2215         // rejection listeners if the message includes an invitation rejection.
   2216         PacketListener declinesListener = new PacketListener() {
   2217             public void processPacket(Packet packet) {
   2218                 // Get the MUC User extension
   2219                 MUCUser mucUser = getMUCUserExtension(packet);
   2220                 // Check if the MUCUser informs that the invitee has declined the invitation
   2221                 if (mucUser.getDecline() != null &&
   2222                         ((Message) packet).getType() != Message.Type.error) {
   2223                     // Fire event for invitation rejection listeners
   2224                     fireInvitationRejectionListeners(
   2225                         mucUser.getDecline().getFrom(),
   2226                         mucUser.getDecline().getReason());
   2227                 }
   2228             }
   2229         };
   2230 
   2231         PacketMultiplexListener packetMultiplexor = new PacketMultiplexListener(
   2232                 messageCollector, presenceListener, subjectListener,
   2233                 declinesListener);
   2234 
   2235         roomListenerMultiplexor = RoomListenerMultiplexor.getRoomMultiplexor(connection);
   2236 
   2237         roomListenerMultiplexor.addRoom(room, packetMultiplexor);
   2238     }
   2239 
   2240     /**
   2241      * Fires notification events if the role of a room occupant has changed. If the occupant that
   2242      * changed his role is your occupant then the <code>UserStatusListeners</code> added to this
   2243      * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
   2244      * his role is not yours then the <code>ParticipantStatusListeners</code> added to this
   2245      * <code>MultiUserChat</code> will be fired. The following table shows the events that will
   2246      * be fired depending on the previous and new role of the occupant.
   2247      *
   2248      * <pre>
   2249      * <table border="1">
   2250      * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
   2251      *
   2252      * <tr><td>None</td><td>Visitor</td><td>--</td></tr>
   2253      * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
   2254      * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
   2255      *
   2256      * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
   2257      * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
   2258      * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
   2259      *
   2260      * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
   2261      * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
   2262      * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
   2263      *
   2264      * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
   2265      * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
   2266      * <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
   2267      * </table>
   2268      * </pre>
   2269      *
   2270      * @param oldRole the previous role of the user in the room before receiving the new presence
   2271      * @param newRole the new role of the user in the room after receiving the new presence
   2272      * @param isUserModification whether the received presence is about your user in the room or not
   2273      * @param from the occupant whose role in the room has changed
   2274      * (e.g. room (at) conference.jabber.org/nick).
   2275      */
   2276     private void checkRoleModifications(
   2277         String oldRole,
   2278         String newRole,
   2279         boolean isUserModification,
   2280         String from) {
   2281         // Voice was granted to a visitor
   2282         if (("visitor".equals(oldRole) || "none".equals(oldRole))
   2283             && "participant".equals(newRole)) {
   2284             if (isUserModification) {
   2285                 fireUserStatusListeners("voiceGranted", new Object[] {});
   2286             }
   2287             else {
   2288                 List<String> params = new ArrayList<String>();
   2289                 params.add(from);
   2290                 fireParticipantStatusListeners("voiceGranted", params);
   2291             }
   2292         }
   2293         // The participant's voice was revoked from the room
   2294         else if (
   2295             "participant".equals(oldRole)
   2296                 && ("visitor".equals(newRole) || "none".equals(newRole))) {
   2297             if (isUserModification) {
   2298                 fireUserStatusListeners("voiceRevoked", new Object[] {});
   2299             }
   2300             else {
   2301                 List<String> params = new ArrayList<String>();
   2302                 params.add(from);
   2303                 fireParticipantStatusListeners("voiceRevoked", params);
   2304             }
   2305         }
   2306         // Moderator privileges were granted to a participant
   2307         if (!"moderator".equals(oldRole) && "moderator".equals(newRole)) {
   2308             if ("visitor".equals(oldRole) || "none".equals(oldRole)) {
   2309                 if (isUserModification) {
   2310                     fireUserStatusListeners("voiceGranted", new Object[] {});
   2311                 }
   2312                 else {
   2313                     List<String> params = new ArrayList<String>();
   2314                     params.add(from);
   2315                     fireParticipantStatusListeners("voiceGranted", params);
   2316                 }
   2317             }
   2318             if (isUserModification) {
   2319                 fireUserStatusListeners("moderatorGranted", new Object[] {});
   2320             }
   2321             else {
   2322                 List<String> params = new ArrayList<String>();
   2323                 params.add(from);
   2324                 fireParticipantStatusListeners("moderatorGranted", params);
   2325             }
   2326         }
   2327         // Moderator privileges were revoked from a participant
   2328         else if ("moderator".equals(oldRole) && !"moderator".equals(newRole)) {
   2329             if ("visitor".equals(newRole) || "none".equals(newRole)) {
   2330                 if (isUserModification) {
   2331                     fireUserStatusListeners("voiceRevoked", new Object[] {});
   2332                 }
   2333                 else {
   2334                     List<String> params = new ArrayList<String>();
   2335                     params.add(from);
   2336                     fireParticipantStatusListeners("voiceRevoked", params);
   2337                 }
   2338             }
   2339             if (isUserModification) {
   2340                 fireUserStatusListeners("moderatorRevoked", new Object[] {});
   2341             }
   2342             else {
   2343                 List<String> params = new ArrayList<String>();
   2344                 params.add(from);
   2345                 fireParticipantStatusListeners("moderatorRevoked", params);
   2346             }
   2347         }
   2348     }
   2349 
   2350     /**
   2351      * Fires notification events if the affiliation of a room occupant has changed. If the
   2352      * occupant that changed his affiliation is your occupant then the
   2353      * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
   2354      * On the other hand, if the occupant that changed his affiliation is not yours then the
   2355      * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
   2356      * fired. The following table shows the events that will be fired depending on the previous
   2357      * and new affiliation of the occupant.
   2358      *
   2359      * <pre>
   2360      * <table border="1">
   2361      * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
   2362      *
   2363      * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
   2364      * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
   2365      * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
   2366      *
   2367      * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
   2368      * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
   2369      * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
   2370      *
   2371      * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
   2372      * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
   2373      * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
   2374      *
   2375      * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
   2376      * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
   2377      * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
   2378      * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
   2379      * </table>
   2380      * </pre>
   2381      *
   2382      * @param oldAffiliation the previous affiliation of the user in the room before receiving the
   2383      * new presence
   2384      * @param newAffiliation the new affiliation of the user in the room after receiving the new
   2385      * presence
   2386      * @param isUserModification whether the received presence is about your user in the room or not
   2387      * @param from the occupant whose role in the room has changed
   2388      * (e.g. room (at) conference.jabber.org/nick).
   2389      */
   2390     private void checkAffiliationModifications(
   2391         String oldAffiliation,
   2392         String newAffiliation,
   2393         boolean isUserModification,
   2394         String from) {
   2395         // First check for revoked affiliation and then for granted affiliations. The idea is to
   2396         // first fire the "revoke" events and then fire the "grant" events.
   2397 
   2398         // The user's ownership to the room was revoked
   2399         if ("owner".equals(oldAffiliation) && !"owner".equals(newAffiliation)) {
   2400             if (isUserModification) {
   2401                 fireUserStatusListeners("ownershipRevoked", new Object[] {});
   2402             }
   2403             else {
   2404                 List<String> params = new ArrayList<String>();
   2405                 params.add(from);
   2406                 fireParticipantStatusListeners("ownershipRevoked", params);
   2407             }
   2408         }
   2409         // The user's administrative privileges to the room were revoked
   2410         else if ("admin".equals(oldAffiliation) && !"admin".equals(newAffiliation)) {
   2411             if (isUserModification) {
   2412                 fireUserStatusListeners("adminRevoked", new Object[] {});
   2413             }
   2414             else {
   2415                 List<String> params = new ArrayList<String>();
   2416                 params.add(from);
   2417                 fireParticipantStatusListeners("adminRevoked", params);
   2418             }
   2419         }
   2420         // The user's membership to the room was revoked
   2421         else if ("member".equals(oldAffiliation) && !"member".equals(newAffiliation)) {
   2422             if (isUserModification) {
   2423                 fireUserStatusListeners("membershipRevoked", new Object[] {});
   2424             }
   2425             else {
   2426                 List<String> params = new ArrayList<String>();
   2427                 params.add(from);
   2428                 fireParticipantStatusListeners("membershipRevoked", params);
   2429             }
   2430         }
   2431 
   2432         // The user was granted ownership to the room
   2433         if (!"owner".equals(oldAffiliation) && "owner".equals(newAffiliation)) {
   2434             if (isUserModification) {
   2435                 fireUserStatusListeners("ownershipGranted", new Object[] {});
   2436             }
   2437             else {
   2438                 List<String> params = new ArrayList<String>();
   2439                 params.add(from);
   2440                 fireParticipantStatusListeners("ownershipGranted", params);
   2441             }
   2442         }
   2443         // The user was granted administrative privileges to the room
   2444         else if (!"admin".equals(oldAffiliation) && "admin".equals(newAffiliation)) {
   2445             if (isUserModification) {
   2446                 fireUserStatusListeners("adminGranted", new Object[] {});
   2447             }
   2448             else {
   2449                 List<String> params = new ArrayList<String>();
   2450                 params.add(from);
   2451                 fireParticipantStatusListeners("adminGranted", params);
   2452             }
   2453         }
   2454         // The user was granted membership to the room
   2455         else if (!"member".equals(oldAffiliation) && "member".equals(newAffiliation)) {
   2456             if (isUserModification) {
   2457                 fireUserStatusListeners("membershipGranted", new Object[] {});
   2458             }
   2459             else {
   2460                 List<String> params = new ArrayList<String>();
   2461                 params.add(from);
   2462                 fireParticipantStatusListeners("membershipGranted", params);
   2463             }
   2464         }
   2465     }
   2466 
   2467     /**
   2468      * Fires events according to the received presence code.
   2469      *
   2470      * @param code
   2471      * @param isUserModification
   2472      * @param mucUser
   2473      * @param from
   2474      */
   2475     private void checkPresenceCode(
   2476         String code,
   2477         boolean isUserModification,
   2478         MUCUser mucUser,
   2479         String from) {
   2480         // Check if an occupant was kicked from the room
   2481         if ("307".equals(code)) {
   2482             // Check if this occupant was kicked
   2483             if (isUserModification) {
   2484                 joined = false;
   2485 
   2486                 fireUserStatusListeners(
   2487                     "kicked",
   2488                     new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
   2489 
   2490                 // Reset occupant information.
   2491                 occupantsMap.clear();
   2492                 nickname = null;
   2493                 userHasLeft();
   2494             }
   2495             else {
   2496                 List<String> params = new ArrayList<String>();
   2497                 params.add(from);
   2498                 params.add(mucUser.getItem().getActor());
   2499                 params.add(mucUser.getItem().getReason());
   2500                 fireParticipantStatusListeners("kicked", params);
   2501             }
   2502         }
   2503         // A user was banned from the room
   2504         else if ("301".equals(code)) {
   2505             // Check if this occupant was banned
   2506             if (isUserModification) {
   2507                 joined = false;
   2508 
   2509                 fireUserStatusListeners(
   2510                     "banned",
   2511                     new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
   2512 
   2513                 // Reset occupant information.
   2514                 occupantsMap.clear();
   2515                 nickname = null;
   2516                 userHasLeft();
   2517             }
   2518             else {
   2519                 List<String> params = new ArrayList<String>();
   2520                 params.add(from);
   2521                 params.add(mucUser.getItem().getActor());
   2522                 params.add(mucUser.getItem().getReason());
   2523                 fireParticipantStatusListeners("banned", params);
   2524             }
   2525         }
   2526         // A user's membership was revoked from the room
   2527         else if ("321".equals(code)) {
   2528             // Check if this occupant's membership was revoked
   2529             if (isUserModification) {
   2530                 joined = false;
   2531 
   2532                 fireUserStatusListeners("membershipRevoked", new Object[] {});
   2533 
   2534                 // Reset occupant information.
   2535                 occupantsMap.clear();
   2536                 nickname = null;
   2537                 userHasLeft();
   2538             }
   2539         }
   2540         // A occupant has changed his nickname in the room
   2541         else if ("303".equals(code)) {
   2542             List<String> params = new ArrayList<String>();
   2543             params.add(from);
   2544             params.add(mucUser.getItem().getNick());
   2545             fireParticipantStatusListeners("nicknameChanged", params);
   2546         }
   2547     }
   2548 
   2549     private void cleanup() {
   2550         try {
   2551             if (connection != null) {
   2552                 roomListenerMultiplexor.removeRoom(room);
   2553                 // Remove all the PacketListeners added to the connection by this chat
   2554                 for (PacketListener connectionListener : connectionListeners) {
   2555                     connection.removePacketListener(connectionListener);
   2556                 }
   2557             }
   2558         } catch (Exception e) {
   2559             // Do nothing
   2560         }
   2561     }
   2562 
   2563     protected void finalize() throws Throwable {
   2564         cleanup();
   2565         super.finalize();
   2566     }
   2567 
   2568     /**
   2569      * An InvitationsMonitor monitors a given connection to detect room invitations. Every
   2570      * time the InvitationsMonitor detects a new invitation it will fire the invitation listeners.
   2571      *
   2572      * @author Gaston Dombiak
   2573      */
   2574     private static class InvitationsMonitor implements ConnectionListener {
   2575         // We use a WeakHashMap so that the GC can collect the monitor when the
   2576         // connection is no longer referenced by any object.
   2577         // Note that when the InvitationsMonitor is used, i.e. when there are InvitationListeners, it will add a
   2578         // PacketListener to the Connection and therefore a strong reference from the Connection to the
   2579         // InvitationsMonior will exists, preventing it from beeing gc'ed. After the last InvitationListener is gone,
   2580         // the PacketListener will get removed (cancel()) allowing the garbage collection of the InvitationsMonitor
   2581         // instance.
   2582         private final static Map<Connection, WeakReference<InvitationsMonitor>> monitors =
   2583                 new WeakHashMap<Connection, WeakReference<InvitationsMonitor>>();
   2584 
   2585         // We don't use a synchronized List here because it would break the semantic of (add|remove)InvitationListener
   2586         private final List<InvitationListener> invitationsListeners =
   2587                 new ArrayList<InvitationListener>();
   2588         private Connection connection;
   2589         private PacketFilter invitationFilter;
   2590         private PacketListener invitationPacketListener;
   2591 
   2592         /**
   2593          * Returns a new or existing InvitationsMonitor for a given connection.
   2594          *
   2595          * @param conn the connection to monitor for room invitations.
   2596          * @return a new or existing InvitationsMonitor for a given connection.
   2597          */
   2598         public static InvitationsMonitor getInvitationsMonitor(Connection conn) {
   2599             synchronized (monitors) {
   2600                 if (!monitors.containsKey(conn) || monitors.get(conn).get() == null) {
   2601                     // We need to use a WeakReference because the monitor references the
   2602                     // connection and this could prevent the GC from collecting the monitor
   2603                     // when no other object references the monitor
   2604                     InvitationsMonitor ivm = new InvitationsMonitor(conn);
   2605                     monitors.put(conn, new WeakReference<InvitationsMonitor>(ivm));
   2606                     return ivm;
   2607                 }
   2608                 // Return the InvitationsMonitor that monitors the connection
   2609                 return monitors.get(conn).get();
   2610             }
   2611         }
   2612 
   2613         /**
   2614          * Creates a new InvitationsMonitor that will monitor invitations received
   2615          * on a given connection.
   2616          *
   2617          * @param connection the connection to monitor for possible room invitations
   2618          */
   2619         private InvitationsMonitor(Connection connection) {
   2620             this.connection = connection;
   2621         }
   2622 
   2623         /**
   2624          * Adds a listener to invitation notifications. The listener will be fired anytime
   2625          * an invitation is received.<p>
   2626          *
   2627          * If this is the first monitor's listener then the monitor will be initialized in
   2628          * order to start listening to room invitations.
   2629          *
   2630          * @param listener an invitation listener.
   2631          */
   2632         public void addInvitationListener(InvitationListener listener) {
   2633             synchronized (invitationsListeners) {
   2634                 // If this is the first monitor's listener then initialize the listeners
   2635                 // on the connection to detect room invitations
   2636                 if (invitationsListeners.size() == 0) {
   2637                     init();
   2638                 }
   2639                 if (!invitationsListeners.contains(listener)) {
   2640                     invitationsListeners.add(listener);
   2641                 }
   2642             }
   2643         }
   2644 
   2645         /**
   2646          * Removes a listener to invitation notifications. The listener will be fired anytime
   2647          * an invitation is received.<p>
   2648          *
   2649          * If there are no more listeners to notifiy for room invitations then the monitor will
   2650          * be stopped. As soon as a new listener is added to the monitor, the monitor will resume
   2651          * monitoring the connection for new room invitations.
   2652          *
   2653          * @param listener an invitation listener.
   2654          */
   2655         public void removeInvitationListener(InvitationListener listener) {
   2656             synchronized (invitationsListeners) {
   2657                 if (invitationsListeners.contains(listener)) {
   2658                     invitationsListeners.remove(listener);
   2659                 }
   2660                 // If there are no more listeners to notifiy for room invitations
   2661                 // then proceed to cancel/release this monitor
   2662                 if (invitationsListeners.size() == 0) {
   2663                     cancel();
   2664                 }
   2665             }
   2666         }
   2667 
   2668         /**
   2669          * Fires invitation listeners.
   2670          */
   2671         private void fireInvitationListeners(String room, String inviter, String reason, String password,
   2672                                              Message message) {
   2673             InvitationListener[] listeners;
   2674             synchronized (invitationsListeners) {
   2675                 listeners = new InvitationListener[invitationsListeners.size()];
   2676                 invitationsListeners.toArray(listeners);
   2677             }
   2678             for (InvitationListener listener : listeners) {
   2679                 listener.invitationReceived(connection, room, inviter, reason, password, message);
   2680             }
   2681         }
   2682 
   2683         public void connectionClosed() {
   2684             cancel();
   2685         }
   2686 
   2687         public void connectionClosedOnError(Exception e) {
   2688             // ignore
   2689         }
   2690 
   2691         public void reconnectingIn(int seconds) {
   2692             // ignore
   2693         }
   2694 
   2695         public void reconnectionSuccessful() {
   2696             // ignore
   2697         }
   2698 
   2699         public void reconnectionFailed(Exception e) {
   2700             // ignore
   2701         }
   2702 
   2703         /**
   2704          * Initializes the listeners to detect received room invitations and to detect when the
   2705          * connection gets closed. As soon as a room invitation is received the invitations
   2706          * listeners will be fired. When the connection gets closed the monitor will remove
   2707          * his listeners on the connection.
   2708          */
   2709         private void init() {
   2710             // Listens for all messages that include a MUCUser extension and fire the invitation
   2711             // listeners if the message includes an invitation.
   2712             invitationFilter =
   2713                 new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user");
   2714             invitationPacketListener = new PacketListener() {
   2715                 public void processPacket(Packet packet) {
   2716                     // Get the MUCUser extension
   2717                     MUCUser mucUser =
   2718                         (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
   2719                     // Check if the MUCUser extension includes an invitation
   2720                     if (mucUser.getInvite() != null &&
   2721                             ((Message) packet).getType() != Message.Type.error) {
   2722                         // Fire event for invitation listeners
   2723                         fireInvitationListeners(packet.getFrom(), mucUser.getInvite().getFrom(),
   2724                                 mucUser.getInvite().getReason(), mucUser.getPassword(), (Message) packet);
   2725                     }
   2726                 }
   2727             };
   2728             connection.addPacketListener(invitationPacketListener, invitationFilter);
   2729             // Add a listener to detect when the connection gets closed in order to
   2730             // cancel/release this monitor
   2731             connection.addConnectionListener(this);
   2732         }
   2733 
   2734         /**
   2735          * Cancels all the listeners that this InvitationsMonitor has added to the connection.
   2736          */
   2737         private void cancel() {
   2738             connection.removePacketListener(invitationPacketListener);
   2739             connection.removeConnectionListener(this);
   2740         }
   2741 
   2742     }
   2743 }
   2744