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
21package org.jivesoftware.smackx.muc;
22
23import java.lang.ref.WeakReference;
24import java.lang.reflect.InvocationTargetException;
25import java.lang.reflect.Method;
26import java.util.ArrayList;
27import java.util.Collection;
28import java.util.Collections;
29import java.util.Iterator;
30import java.util.List;
31import java.util.Map;
32import java.util.WeakHashMap;
33import java.util.concurrent.ConcurrentHashMap;
34
35import org.jivesoftware.smack.Chat;
36import org.jivesoftware.smack.ConnectionCreationListener;
37import org.jivesoftware.smack.ConnectionListener;
38import org.jivesoftware.smack.MessageListener;
39import org.jivesoftware.smack.PacketCollector;
40import org.jivesoftware.smack.PacketInterceptor;
41import org.jivesoftware.smack.PacketListener;
42import org.jivesoftware.smack.SmackConfiguration;
43import org.jivesoftware.smack.Connection;
44import org.jivesoftware.smack.XMPPException;
45import org.jivesoftware.smack.filter.AndFilter;
46import org.jivesoftware.smack.filter.FromMatchesFilter;
47import org.jivesoftware.smack.filter.MessageTypeFilter;
48import org.jivesoftware.smack.filter.PacketExtensionFilter;
49import org.jivesoftware.smack.filter.PacketFilter;
50import org.jivesoftware.smack.filter.PacketIDFilter;
51import org.jivesoftware.smack.filter.PacketTypeFilter;
52import org.jivesoftware.smack.packet.IQ;
53import org.jivesoftware.smack.packet.Message;
54import org.jivesoftware.smack.packet.Packet;
55import org.jivesoftware.smack.packet.PacketExtension;
56import org.jivesoftware.smack.packet.Presence;
57import org.jivesoftware.smack.packet.Registration;
58import org.jivesoftware.smackx.Form;
59import org.jivesoftware.smackx.NodeInformationProvider;
60import org.jivesoftware.smackx.ServiceDiscoveryManager;
61import org.jivesoftware.smackx.packet.DiscoverInfo;
62import org.jivesoftware.smackx.packet.DiscoverItems;
63import org.jivesoftware.smackx.packet.MUCAdmin;
64import org.jivesoftware.smackx.packet.MUCInitialPresence;
65import org.jivesoftware.smackx.packet.MUCOwner;
66import 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 */
78public 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@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@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@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@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@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@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@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@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@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@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@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@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@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@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@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@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@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@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@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