001/**
002 *
003 * Copyright 2003-2007 Jive Software.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.jivesoftware.smackx.muc;
019
020import java.util.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Set;
027import java.util.concurrent.ConcurrentHashMap;
028import java.util.concurrent.CopyOnWriteArraySet;
029import java.util.logging.Level;
030import java.util.logging.Logger;
031
032import org.jivesoftware.smack.MessageListener;
033import org.jivesoftware.smack.PacketCollector;
034import org.jivesoftware.smack.StanzaListener;
035import org.jivesoftware.smack.PresenceListener;
036import org.jivesoftware.smack.SmackException;
037import org.jivesoftware.smack.SmackException.NoResponseException;
038import org.jivesoftware.smack.SmackException.NotConnectedException;
039import org.jivesoftware.smack.XMPPConnection;
040import org.jivesoftware.smack.XMPPException;
041import org.jivesoftware.smack.XMPPException.XMPPErrorException;
042import org.jivesoftware.smack.chat.Chat;
043import org.jivesoftware.smack.chat.ChatManager;
044import org.jivesoftware.smack.chat.ChatMessageListener;
045import org.jivesoftware.smack.filter.AndFilter;
046import org.jivesoftware.smack.filter.FromMatchesFilter;
047import org.jivesoftware.smack.filter.MessageTypeFilter;
048import org.jivesoftware.smack.filter.MessageWithSubjectFilter;
049import org.jivesoftware.smack.filter.NotFilter;
050import org.jivesoftware.smack.filter.StanzaFilter;
051import org.jivesoftware.smack.filter.StanzaExtensionFilter;
052import org.jivesoftware.smack.filter.StanzaTypeFilter;
053import org.jivesoftware.smack.filter.ToFilter;
054import org.jivesoftware.smack.packet.IQ;
055import org.jivesoftware.smack.packet.Message;
056import org.jivesoftware.smack.packet.Stanza;
057import org.jivesoftware.smack.packet.Presence;
058import org.jivesoftware.smack.util.StringUtils;
059import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
060import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
061import org.jivesoftware.smackx.iqregister.packet.Registration;
062import org.jivesoftware.smackx.muc.packet.Destroy;
063import org.jivesoftware.smackx.muc.packet.MUCAdmin;
064import org.jivesoftware.smackx.muc.packet.MUCInitialPresence;
065import org.jivesoftware.smackx.muc.packet.MUCItem;
066import org.jivesoftware.smackx.muc.packet.MUCOwner;
067import org.jivesoftware.smackx.muc.packet.MUCUser;
068import org.jivesoftware.smackx.muc.packet.MUCUser.Status;
069import org.jivesoftware.smackx.xdata.Form;
070import org.jivesoftware.smackx.xdata.FormField;
071import org.jivesoftware.smackx.xdata.packet.DataForm;
072
073/**
074 * A MultiUserChat room (XEP-45), created with {@link MultiUserChatManager#getMultiUserChat(String)}.
075 * <p>
076 * A MultiUserChat is a conversation that takes place among many users in a virtual
077 * room. A room could have many occupants with different affiliation and roles.
078 * Possible affiliations are "owner", "admin", "member", and "outcast". Possible roles
079 * are "moderator", "participant", and "visitor". Each role and affiliation guarantees
080 * different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
081 * Grant voice, Edit member list, etc.).
082 * </p>
083 * <p>
084 * <b>Note:</b> Make sure to leave the MUC ({@link #leave()}) when you don't need it anymore or
085 * otherwise you may leak the instance.
086 * </p>
087 *
088 * @author Gaston Dombiak, Larry Kirschner
089 */
090public class MultiUserChat {
091    private static final Logger LOGGER = Logger.getLogger(MultiUserChat.class.getName());
092
093    private final XMPPConnection connection;
094    private final String room;
095    private final MultiUserChatManager multiUserChatManager;
096    private final Map<String, Presence> occupantsMap = new ConcurrentHashMap<String, Presence>();
097
098    private final Set<InvitationRejectionListener> invitationRejectionListeners = new CopyOnWriteArraySet<InvitationRejectionListener>();
099    private final Set<SubjectUpdatedListener> subjectUpdatedListeners = new CopyOnWriteArraySet<SubjectUpdatedListener>();
100    private final Set<UserStatusListener> userStatusListeners = new CopyOnWriteArraySet<UserStatusListener>();
101    private final Set<ParticipantStatusListener> participantStatusListeners = new CopyOnWriteArraySet<ParticipantStatusListener>();
102    private final Set<MessageListener> messageListeners = new CopyOnWriteArraySet<MessageListener>();
103    private final Set<PresenceListener> presenceListeners = new CopyOnWriteArraySet<PresenceListener>();
104    private final Set<PresenceListener> presenceInterceptors = new CopyOnWriteArraySet<PresenceListener>();
105
106    /**
107     * This filter will match all stanzas send from the groupchat or from one if
108     * the groupchat participants, i.e. it filters only the bare JID of the from
109     * attribute against the JID of the MUC.
110     */
111    private final StanzaFilter fromRoomFilter;
112
113    /**
114     * Same as {@link #fromRoomFilter} together with {@link MessageTypeFilter#GROUPCHAT}.
115     */
116    private final StanzaFilter fromRoomGroupchatFilter;
117
118    private final StanzaListener presenceInterceptor;
119    private final StanzaListener messageListener;
120    private final StanzaListener presenceListener;
121    private final StanzaListener subjectListener;
122    private final StanzaListener declinesListener;
123
124    private String subject;
125    private String nickname = null;
126    private boolean joined = false;
127    private PacketCollector messageCollector;
128
129    MultiUserChat(XMPPConnection connection, String room, MultiUserChatManager multiUserChatManager) {
130        this.connection = connection;
131        this.room = room.toLowerCase(Locale.US);
132        this.multiUserChatManager = multiUserChatManager;
133
134        fromRoomFilter = FromMatchesFilter.create(room);
135        fromRoomGroupchatFilter = new AndFilter(fromRoomFilter, MessageTypeFilter.GROUPCHAT);
136
137        messageListener = new StanzaListener() {
138            @Override
139            public void processPacket(Stanza packet) throws NotConnectedException {
140                Message message = (Message) packet;
141                for (MessageListener listener : messageListeners) {
142                    listener.processMessage(message);
143                }
144            }
145        };
146
147        // Create a listener for subject updates.
148        subjectListener = new StanzaListener() {
149            public void processPacket(Stanza packet) {
150                Message msg = (Message) packet;
151                // Update the room subject
152                subject = msg.getSubject();
153                // Fire event for subject updated listeners
154                for (SubjectUpdatedListener listener : subjectUpdatedListeners) {
155                    listener.subjectUpdated(subject, msg.getFrom());
156                }
157            }
158        };
159
160        // Create a listener for all presence updates.
161        presenceListener = new StanzaListener() {
162            public void processPacket(Stanza packet) {
163                Presence presence = (Presence) packet;
164                String from = presence.getFrom();
165                String myRoomJID = MultiUserChat.this.room + "/" + nickname;
166                boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
167                switch (presence.getType()) {
168                case available:
169                    Presence oldPresence = occupantsMap.put(from, presence);
170                    if (oldPresence != null) {
171                        // Get the previous occupant's affiliation & role
172                        MUCUser mucExtension = MUCUser.from(oldPresence);
173                        MUCAffiliation oldAffiliation = mucExtension.getItem().getAffiliation();
174                        MUCRole oldRole = mucExtension.getItem().getRole();
175                        // Get the new occupant's affiliation & role
176                        mucExtension = MUCUser.from(packet);
177                        MUCAffiliation newAffiliation = mucExtension.getItem().getAffiliation();
178                        MUCRole newRole = mucExtension.getItem().getRole();
179                        // Fire role modification events
180                        checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
181                        // Fire affiliation modification events
182                        checkAffiliationModifications(
183                            oldAffiliation,
184                            newAffiliation,
185                            isUserStatusModification,
186                            from);
187                    }
188                    else {
189                        // A new occupant has joined the room
190                        if (!isUserStatusModification) {
191                            for (ParticipantStatusListener listener : participantStatusListeners) {
192                                listener.joined(from);
193                            }
194                        }
195                    }
196                    break;
197                case unavailable:
198                    occupantsMap.remove(from);
199                    MUCUser mucUser = MUCUser.from(packet);
200                    if (mucUser != null && mucUser.hasStatus()) {
201                        // Fire events according to the received presence code
202                        checkPresenceCode(
203                            mucUser.getStatus(),
204                            presence.getFrom().equals(myRoomJID),
205                            mucUser,
206                            from);
207                    } else {
208                        // An occupant has left the room
209                        if (!isUserStatusModification) {
210                            for (ParticipantStatusListener listener : participantStatusListeners) {
211                                listener.left(from);
212                            }
213                        }
214                    }
215                    break;
216                default:
217                    break;
218                }
219                for (PresenceListener listener : presenceListeners) {
220                    listener.processPresence(presence);
221                }
222            }
223        };
224
225        // Listens for all messages that include a MUCUser extension and fire the invitation
226        // rejection listeners if the message includes an invitation rejection.
227        declinesListener = new StanzaListener() {
228            public void processPacket(Stanza packet) {
229                // Get the MUC User extension
230                MUCUser mucUser = MUCUser.from(packet);
231                // Check if the MUCUser informs that the invitee has declined the invitation
232                if (mucUser.getDecline() == null) {
233                    return;
234                }
235                // Fire event for invitation rejection listeners
236                fireInvitationRejectionListeners(mucUser.getDecline().getFrom(), mucUser.getDecline().getReason());
237            }
238        };
239
240        presenceInterceptor = new StanzaListener() {
241            @Override
242            public void processPacket(Stanza packet) {
243                Presence presence = (Presence) packet;
244                for (PresenceListener interceptor : presenceInterceptors) {
245                    interceptor.processPresence(presence);
246                }
247            }
248        };
249    }
250
251
252    /**
253     * Returns the name of the room this MultiUserChat object represents.
254     *
255     * @return the multi user chat room name.
256     */
257    public String getRoom() {
258        return room;
259    }
260
261    /**
262     * Enter a room, as described in XEP-45 7.2.
263     *
264     * @param nickname
265     * @param password
266     * @param history
267     * @param timeout
268     * @return the returned presence by the service after the client send the initial presence in order to enter the room.
269     * @throws NotConnectedException
270     * @throws NoResponseException
271     * @throws XMPPErrorException
272     * @see <a href="http://xmpp.org/extensions/xep-0045.html#enter">XEP-45 7.2 Entering a Room</a>
273     */
274    private Presence enter(String nickname, String password, DiscussionHistory history,
275                    long timeout) throws NotConnectedException, NoResponseException,
276                    XMPPErrorException {
277        StringUtils.requireNotNullOrEmpty(nickname, "Nickname must not be null or blank.");
278        // We enter a room by sending a presence packet where the "to"
279        // field is in the form "roomName@service/nickname"
280        Presence joinPresence = new Presence(Presence.Type.available);
281        joinPresence.setTo(room + "/" + nickname);
282
283        // Indicate the the client supports MUC
284        MUCInitialPresence mucInitialPresence = new MUCInitialPresence();
285        if (password != null) {
286            mucInitialPresence.setPassword(password);
287        }
288        if (history != null) {
289            mucInitialPresence.setHistory(history.getMUCHistory());
290        }
291        joinPresence.addExtension(mucInitialPresence);
292
293        // Wait for a presence packet back from the server.
294        StanzaFilter responseFilter = new AndFilter(FromMatchesFilter.createFull(room + "/"
295                        + nickname), new StanzaTypeFilter(Presence.class));
296
297        // Setup the messageListeners and presenceListeners *before* the join presence is send.
298        connection.addSyncStanzaListener(messageListener, fromRoomGroupchatFilter);
299        connection.addSyncStanzaListener(presenceListener, new AndFilter(fromRoomFilter,
300                        StanzaTypeFilter.PRESENCE));
301        connection.addSyncStanzaListener(subjectListener, new AndFilter(fromRoomFilter,
302                        MessageWithSubjectFilter.INSTANCE));
303        connection.addSyncStanzaListener(declinesListener, new AndFilter(new StanzaExtensionFilter(MUCUser.ELEMENT,
304                        MUCUser.NAMESPACE), new NotFilter(MessageTypeFilter.ERROR)));
305        connection.addPacketInterceptor(presenceInterceptor, new AndFilter(new ToFilter(room),
306                        StanzaTypeFilter.PRESENCE));
307        messageCollector = connection.createPacketCollector(fromRoomGroupchatFilter);
308
309        Presence presence;
310        try {
311            presence = connection.createPacketCollectorAndSend(responseFilter, joinPresence).nextResultOrThrow(timeout);
312        }
313        catch (NoResponseException | XMPPErrorException e) {
314            // Ensure that all callbacks are removed if there is an exception
315            removeConnectionCallbacks();
316            throw e;
317        }
318
319        this.nickname = nickname;
320        joined = true;
321
322        // Update the list of joined rooms
323        multiUserChatManager.addJoinedRoom(room);
324        return presence;
325    }
326
327    /**
328     * Creates the room according to some default configuration, assign the requesting user as the
329     * room owner, and add the owner to the room but not allow anyone else to enter the room
330     * (effectively "locking" the room). The requesting user will join the room under the specified
331     * nickname as soon as the room has been created.
332     * <p>
333     * To create an "Instant Room", that means a room with some default configuration that is
334     * available for immediate access, the room's owner should send an empty form after creating the
335     * room. {@link #sendConfigurationForm(Form)}
336     * <p>
337     * To create a "Reserved Room", that means a room manually configured by the room creator before
338     * anyone is allowed to enter, the room's owner should complete and send a form after creating
339     * the room. Once the completed configuration form is sent to the server, the server will unlock
340     * the room. {@link #sendConfigurationForm(Form)}
341     * 
342     * @param nickname the nickname to use.
343     * @throws XMPPErrorException if the room couldn't be created for some reason (e.g. 405 error if
344     *         the user is not allowed to create the room)
345     * @throws NoResponseException if there was no response from the server.
346     * @throws SmackException If the creation failed because of a missing acknowledge from the
347     *         server, e.g. because the room already existed.
348     */
349    public synchronized void create(String nickname) throws NoResponseException, XMPPErrorException, SmackException {
350        if (joined) {
351            throw new IllegalStateException("Creation failed - User already joined the room.");
352        }
353
354        if (createOrJoin(nickname)) {
355            // We successfully created a new room
356            return;
357        }
358        // We need to leave the room since it seems that the room already existed
359        leave();
360        throw new SmackException("Creation failed - Missing acknowledge of room creation.");
361    }
362
363    /**
364     * Same as {@link #createOrJoin(String, String, DiscussionHistory, long)}, but without a password, specifying a
365     * discussion history and using the connections default reply timeout.
366     * 
367     * @param nickname
368     * @return true if the room creation was acknowledged by the service, false otherwise.
369     * @throws NoResponseException
370     * @throws XMPPErrorException
371     * @throws SmackException
372     * @see #createOrJoin(String, String, DiscussionHistory, long)
373     */
374    public synchronized boolean createOrJoin(String nickname) throws NoResponseException, XMPPErrorException,
375                    SmackException {
376        return createOrJoin(nickname, null, null, connection.getPacketReplyTimeout());
377    }
378
379    /**
380     * Like {@link #create(String)}, but will return true if the room creation was acknowledged by
381     * the service (with an 201 status code). It's up to the caller to decide, based on the return
382     * value, if he needs to continue sending the room configuration. If false is returned, the room
383     * already existed and the user is able to join right away, without sending a form.
384     *
385     * @param nickname the nickname to use.
386     * @param password the password to use.
387     * @param history the amount of discussion history to receive while joining a room.
388     * @param timeout the amount of time to wait for a reply from the MUC service(in milliseconds).
389     * @return true if the room creation was acknowledged by the service, false otherwise.
390     * @throws XMPPErrorException if the room couldn't be created for some reason (e.g. 405 error if
391     *         the user is not allowed to create the room)
392     * @throws NoResponseException if there was no response from the server.
393     */
394    public synchronized boolean createOrJoin(String nickname, String password, DiscussionHistory history, long timeout)
395                    throws NoResponseException, XMPPErrorException, SmackException {
396        if (joined) {
397            throw new IllegalStateException("Creation failed - User already joined the room.");
398        }
399
400        Presence presence = enter(nickname, password, history, timeout);
401
402        // Look for confirmation of room creation from the server
403        MUCUser mucUser = MUCUser.from(presence);
404        if (mucUser != null && mucUser.getStatus().contains(Status.ROOM_CREATED_201)) {
405            // Room was created and the user has joined the room
406            return true;
407        }
408        return false;
409    }
410
411    /**
412     * Joins the chat room using the specified nickname. If already joined
413     * using another nickname, this method will first leave the room and then
414     * re-join using the new nickname. The default connection timeout for a reply
415     * from the group chat server that the join succeeded will be used. After
416     * joining the room, the room will decide the amount of history to send.
417     *
418     * @param nickname the nickname to use.
419     * @throws NoResponseException 
420     * @throws XMPPErrorException if an error occurs joining the room. In particular, a
421     *      401 error can occur if no password was provided and one is required; or a
422     *      403 error can occur if the user is banned; or a
423     *      404 error can occur if the room does not exist or is locked; or a
424     *      407 error can occur if user is not on the member list; or a
425     *      409 error can occur if someone is already in the group chat with the same nickname.
426     * @throws NoResponseException if there was no response from the server.
427     * @throws NotConnectedException 
428     */
429    public void join(String nickname) throws NoResponseException, XMPPErrorException, NotConnectedException {
430        join(nickname, null, null, connection.getPacketReplyTimeout());
431    }
432
433    /**
434     * Joins the chat room using the specified nickname and password. If already joined
435     * using another nickname, this method will first leave the room and then
436     * re-join using the new nickname. The default connection timeout for a reply
437     * from the group chat server that the join succeeded will be used. After
438     * joining the room, the room will decide the amount of history to send.<p>
439     *
440     * A password is required when joining password protected rooms. If the room does
441     * not require a password there is no need to provide one.
442     *
443     * @param nickname the nickname to use.
444     * @param password the password to use.
445     * @throws XMPPErrorException if an error occurs joining the room. In particular, a
446     *      401 error can occur if no password was provided and one is required; or a
447     *      403 error can occur if the user is banned; or a
448     *      404 error can occur if the room does not exist or is locked; or a
449     *      407 error can occur if user is not on the member list; or a
450     *      409 error can occur if someone is already in the group chat with the same nickname.
451     * @throws SmackException if there was no response from the server.
452     */
453    public void join(String nickname, String password) throws XMPPErrorException, SmackException {
454        join(nickname, password, null, connection.getPacketReplyTimeout());
455    }
456
457    /**
458     * Joins the chat room using the specified nickname and password. If already joined
459     * using another nickname, this method will first leave the room and then
460     * re-join using the new nickname.<p>
461     *
462     * To control the amount of history to receive while joining a room you will need to provide
463     * a configured DiscussionHistory object.<p>
464     *
465     * A password is required when joining password protected rooms. If the room does
466     * not require a password there is no need to provide one.<p>
467     *
468     * If the room does not already exist when the user seeks to enter it, the server will
469     * decide to create a new room or not.
470     *
471     * @param nickname the nickname to use.
472     * @param password the password to use.
473     * @param history the amount of discussion history to receive while joining a room.
474     * @param timeout the amount of time to wait for a reply from the MUC service(in milleseconds).
475     * @throws XMPPErrorException if an error occurs joining the room. In particular, a
476     *      401 error can occur if no password was provided and one is required; or a
477     *      403 error can occur if the user is banned; or a
478     *      404 error can occur if the room does not exist or is locked; or a
479     *      407 error can occur if user is not on the member list; or a
480     *      409 error can occur if someone is already in the group chat with the same nickname.
481     * @throws NoResponseException if there was no response from the server.
482     * @throws NotConnectedException 
483     */
484    public synchronized void join(
485        String nickname,
486        String password,
487        DiscussionHistory history,
488        long timeout)
489        throws XMPPErrorException, NoResponseException, NotConnectedException {
490        // If we've already joined the room, leave it before joining under a new
491        // nickname.
492        if (joined) {
493            leave();
494        }
495        enter(nickname, password, history, timeout);
496    }
497
498    /**
499     * Returns true if currently in the multi user chat (after calling the {@link
500     * #join(String)} method).
501     *
502     * @return true if currently in the multi user chat room.
503     */
504    public boolean isJoined() {
505        return joined;
506    }
507
508    /**
509     * Leave the chat room.
510     * @throws NotConnectedException 
511     */
512    public synchronized void leave() throws NotConnectedException {
513        // If not joined already, do nothing.
514        if (!joined) {
515            return;
516        }
517        // We leave a room by sending a presence packet where the "to"
518        // field is in the form "roomName@service/nickname"
519        Presence leavePresence = new Presence(Presence.Type.unavailable);
520        leavePresence.setTo(room + "/" + nickname);
521        connection.sendStanza(leavePresence);
522        // Reset occupant information.
523        occupantsMap.clear();
524        nickname = null;
525        joined = false;
526        userHasLeft();
527    }
528
529    /**
530     * Returns the room's configuration form that the room's owner can use or <tt>null</tt> if
531     * no configuration is possible. The configuration form allows to set the room's language,
532     * enable logging, specify room's type, etc..
533     *
534     * @return the Form that contains the fields to complete together with the instrucions or
535     * <tt>null</tt> if no configuration is possible.
536     * @throws XMPPErrorException if an error occurs asking the configuration form for the room.
537     * @throws NoResponseException if there was no response from the server.
538     * @throws NotConnectedException 
539     */
540    public Form getConfigurationForm() throws NoResponseException, XMPPErrorException, NotConnectedException {
541        MUCOwner iq = new MUCOwner();
542        iq.setTo(room);
543        iq.setType(IQ.Type.get);
544
545        IQ answer = connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
546        return Form.getFormFrom(answer);
547    }
548
549    /**
550     * Sends the completed configuration form to the server. The room will be configured
551     * with the new settings defined in the form. If the form is empty then the server
552     * will create an instant room (will use default configuration).
553     *
554     * @param form the form with the new settings.
555     * @throws XMPPErrorException if an error occurs setting the new rooms' configuration.
556     * @throws NoResponseException if there was no response from the server.
557     * @throws NotConnectedException 
558     */
559    public void sendConfigurationForm(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException {
560        MUCOwner iq = new MUCOwner();
561        iq.setTo(room);
562        iq.setType(IQ.Type.set);
563        iq.addExtension(form.getDataFormToSend());
564
565        connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
566    }
567
568    /**
569     * Returns the room's registration form that an unaffiliated user, can use to become a member
570     * of the room or <tt>null</tt> if no registration is possible. Some rooms may restrict the
571     * privilege to register members and allow only room admins to add new members.<p>
572     *
573     * If the user requesting registration requirements is not allowed to register with the room
574     * (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
575     * error to the user (error code 405).
576     *
577     * @return the registration Form that contains the fields to complete together with the
578     * instrucions or <tt>null</tt> if no registration is possible.
579     * @throws XMPPErrorException if an error occurs asking the registration form for the room or a
580     * 405 error if the user is not allowed to register with the room.
581     * @throws NoResponseException if there was no response from the server.
582     * @throws NotConnectedException 
583     */
584    public Form getRegistrationForm() throws NoResponseException, XMPPErrorException, NotConnectedException {
585        Registration reg = new Registration();
586        reg.setType(IQ.Type.get);
587        reg.setTo(room);
588
589        IQ result = connection.createPacketCollectorAndSend(reg).nextResultOrThrow();
590        return Form.getFormFrom(result);
591    }
592
593    /**
594     * Sends the completed registration form to the server. After the user successfully submits
595     * the form, the room may queue the request for review by the room admins or may immediately
596     * add the user to the member list by changing the user's affiliation from "none" to "member.<p>
597     *
598     * If the desired room nickname is already reserved for that room, the room will return a
599     * "Conflict" error to the user (error code 409). If the room does not support registration,
600     * it will return a "Service Unavailable" error to the user (error code 503).
601     *
602     * @param form the completed registration form.
603     * @throws XMPPErrorException if an error occurs submitting the registration form. In particular, a
604     *      409 error can occur if the desired room nickname is already reserved for that room;
605     *      or a 503 error can occur if the room does not support registration.
606     * @throws NoResponseException if there was no response from the server.
607     * @throws NotConnectedException 
608     */
609    public void sendRegistrationForm(Form form) throws NoResponseException, XMPPErrorException, NotConnectedException {
610        Registration reg = new Registration();
611        reg.setType(IQ.Type.set);
612        reg.setTo(room);
613        reg.addExtension(form.getDataFormToSend());
614
615        connection.createPacketCollectorAndSend(reg).nextResultOrThrow();
616    }
617
618    /**
619     * Sends a request to the server to destroy the room. The sender of the request
620     * should be the room's owner. If the sender of the destroy request is not the room's owner
621     * then the server will answer a "Forbidden" error (403).
622     *
623     * @param reason the reason for the room destruction.
624     * @param alternateJID the JID of an alternate location.
625     * @throws XMPPErrorException if an error occurs while trying to destroy the room.
626     *      An error can occur which will be wrapped by an XMPPException --
627     *      XMPP error code 403. The error code can be used to present more
628     *      appropiate error messages to end-users.
629     * @throws NoResponseException if there was no response from the server.
630     * @throws NotConnectedException 
631     */
632    public void destroy(String reason, String alternateJID) throws NoResponseException, XMPPErrorException, NotConnectedException {
633        MUCOwner iq = new MUCOwner();
634        iq.setTo(room);
635        iq.setType(IQ.Type.set);
636
637        // Create the reason for the room destruction
638        Destroy destroy = new Destroy();
639        destroy.setReason(reason);
640        destroy.setJid(alternateJID);
641        iq.setDestroy(destroy);
642
643        connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
644
645        // Reset occupant information.
646        occupantsMap.clear();
647        nickname = null;
648        joined = false;
649        userHasLeft();
650    }
651
652    /**
653     * Invites another user to the room in which one is an occupant. The invitation
654     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
655     *
656     * If the room is password-protected, the invitee will receive a password to use to join
657     * the room. If the room is members-only, the the invitee may be added to the member list.
658     *
659     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
660     * @param reason the reason why the user is being invited.
661     * @throws NotConnectedException 
662     */
663    public void invite(String user, String reason) throws NotConnectedException {
664        invite(new Message(), user, reason);
665    }
666
667    /**
668     * Invites another user to the room in which one is an occupant using a given Message. The invitation
669     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
670     *
671     * If the room is password-protected, the invitee will receive a password to use to join
672     * the room. If the room is members-only, the the invitee may be added to the member list.
673     *
674     * @param message the message to use for sending the invitation.
675     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
676     * @param reason the reason why the user is being invited.
677     * @throws NotConnectedException 
678     */
679    public void invite(Message message, String user, String reason) throws NotConnectedException {
680        // TODO listen for 404 error code when inviter supplies a non-existent JID
681        message.setTo(room);
682
683        // Create the MUCUser packet that will include the invitation
684        MUCUser mucUser = new MUCUser();
685        MUCUser.Invite invite = new MUCUser.Invite();
686        invite.setTo(user);
687        invite.setReason(reason);
688        mucUser.setInvite(invite);
689        // Add the MUCUser packet that includes the invitation to the message
690        message.addExtension(mucUser);
691
692        connection.sendStanza(message);
693    }
694
695    /**
696     * Adds a listener to invitation rejections notifications. The listener will be fired anytime
697     * an invitation is declined.
698     *
699     * @param listener an invitation rejection listener.
700     * @return true if the listener was not already added.
701     */
702    public boolean addInvitationRejectionListener(InvitationRejectionListener listener) {
703         return invitationRejectionListeners.add(listener);
704    }
705
706    /**
707     * Removes a listener from invitation rejections notifications. The listener will be fired
708     * anytime an invitation is declined.
709     *
710     * @param listener an invitation rejection listener.
711     * @return true if the listener was registered and is now removed.
712     */
713    public boolean removeInvitationRejectionListener(InvitationRejectionListener listener) {
714        return invitationRejectionListeners.remove(listener);
715    }
716
717    /**
718     * Fires invitation rejection listeners.
719     *
720     * @param invitee the user being invited.
721     * @param reason the reason for the rejection
722     */
723    private void fireInvitationRejectionListeners(String invitee, String reason) {
724        InvitationRejectionListener[] listeners;
725        synchronized (invitationRejectionListeners) {
726            listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
727            invitationRejectionListeners.toArray(listeners);
728        }
729        for (InvitationRejectionListener listener : listeners) {
730            listener.invitationDeclined(invitee, reason);
731        }
732    }
733
734    /**
735     * Adds a listener to subject change notifications. The listener will be fired anytime
736     * the room's subject changes.
737     *
738     * @param listener a subject updated listener.
739     * @return true if the listener was not already added.
740     */
741    public boolean addSubjectUpdatedListener(SubjectUpdatedListener listener) {
742        return subjectUpdatedListeners.add(listener);
743    }
744
745    /**
746     * Removes a listener from subject change notifications. The listener will be fired
747     * anytime the room's subject changes.
748     *
749     * @param listener a subject updated listener.
750     * @return true if the listener was registered and is now removed.
751     */
752    public boolean removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
753        return subjectUpdatedListeners.remove(listener);
754    }
755
756    /**
757     * Adds a new {@link StanzaListener} that will be invoked every time a new presence
758     * is going to be sent by this MultiUserChat to the server. Stanza(/Packet) interceptors may
759     * add new extensions to the presence that is going to be sent to the MUC service.
760     *
761     * @param presenceInterceptor the new stanza(/packet) interceptor that will intercept presence packets.
762     */
763    public void addPresenceInterceptor(PresenceListener presenceInterceptor) {
764        presenceInterceptors.add(presenceInterceptor);
765    }
766
767    /**
768     * Removes a {@link StanzaListener} that was being invoked every time a new presence
769     * was being sent by this MultiUserChat to the server. Stanza(/Packet) interceptors may
770     * add new extensions to the presence that is going to be sent to the MUC service.
771     *
772     * @param presenceInterceptor the stanza(/packet) interceptor to remove.
773     */
774    public void removePresenceInterceptor(StanzaListener presenceInterceptor) {
775        presenceInterceptors.remove(presenceInterceptor);
776    }
777
778    /**
779     * Returns the last known room's subject or <tt>null</tt> if the user hasn't joined the room
780     * or the room does not have a subject yet. In case the room has a subject, as soon as the
781     * user joins the room a message with the current room's subject will be received.<p>
782     *
783     * To be notified every time the room's subject change you should add a listener
784     * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
785     *
786     * To change the room's subject use {@link #changeSubject(String)}.
787     *
788     * @return the room's subject or <tt>null</tt> if the user hasn't joined the room or the
789     * room does not have a subject yet.
790     */
791    public String getSubject() {
792        return subject;
793    }
794
795    /**
796     * Returns the reserved room nickname for the user in the room. A user may have a reserved
797     * nickname, for example through explicit room registration or database integration. In such
798     * cases it may be desirable for the user to discover the reserved nickname before attempting
799     * to enter the room.
800     *
801     * @return the reserved room nickname or <tt>null</tt> if none.
802     * @throws SmackException if there was no response from the server.
803     */
804    public String getReservedNickname() throws SmackException {
805        try {
806            DiscoverInfo result =
807                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
808                    room,
809                    "x-roomuser-item");
810            // Look for an Identity that holds the reserved nickname and return its name
811            for (DiscoverInfo.Identity identity : result.getIdentities()) {
812                return identity.getName();
813            }
814        }
815        catch (XMPPException e) {
816            LOGGER.log(Level.SEVERE, "Error retrieving room nickname", e);
817        }
818        // If no Identity was found then the user does not have a reserved room nickname
819        return null;
820    }
821
822    /**
823     * Returns the nickname that was used to join the room, or <tt>null</tt> if not
824     * currently joined.
825     *
826     * @return the nickname currently being used.
827     */
828    public String getNickname() {
829        return nickname;
830    }
831
832    /**
833     * Changes the occupant's nickname to a new nickname within the room. Each room occupant
834     * will receive two presence packets. One of type "unavailable" for the old nickname and one
835     * indicating availability for the new nickname. The unavailable presence will contain the new
836     * nickname and an appropriate status code (namely 303) as extended presence information. The
837     * status code 303 indicates that the occupant is changing his/her nickname.
838     *
839     * @param nickname the new nickname within the room.
840     * @throws XMPPErrorException if the new nickname is already in use by another occupant.
841     * @throws NoResponseException if there was no response from the server.
842     * @throws NotConnectedException 
843     */
844    public void changeNickname(String nickname) throws NoResponseException, XMPPErrorException, NotConnectedException  {
845        StringUtils.requireNotNullOrEmpty(nickname, "Nickname must not be null or blank.");
846        // Check that we already have joined the room before attempting to change the
847        // nickname.
848        if (!joined) {
849            throw new IllegalStateException("Must be logged into the room to change nickname.");
850        }
851        // We change the nickname by sending a presence packet where the "to"
852        // field is in the form "roomName@service/nickname"
853        // We don't have to signal the MUC support again
854        Presence joinPresence = new Presence(Presence.Type.available);
855        joinPresence.setTo(room + "/" + nickname);
856
857        // Wait for a presence packet back from the server.
858        StanzaFilter responseFilter =
859            new AndFilter(
860                FromMatchesFilter.createFull(room + "/" + nickname),
861                new StanzaTypeFilter(Presence.class));
862        PacketCollector response = connection.createPacketCollectorAndSend(responseFilter, joinPresence);
863        // Wait up to a certain number of seconds for a reply. If there is a negative reply, an
864        // exception will be thrown
865        response.nextResultOrThrow();
866
867        this.nickname = nickname;
868    }
869
870    /**
871     * Changes the occupant's availability status within the room. The presence type
872     * will remain available but with a new status that describes the presence update and
873     * a new presence mode (e.g. Extended away).
874     *
875     * @param status a text message describing the presence update.
876     * @param mode the mode type for the presence update.
877     * @throws NotConnectedException 
878     */
879    public void changeAvailabilityStatus(String status, Presence.Mode mode) throws NotConnectedException {
880        StringUtils.requireNotNullOrEmpty(nickname, "Nickname must not be null or blank.");
881        // Check that we already have joined the room before attempting to change the
882        // availability status.
883        if (!joined) {
884            throw new IllegalStateException(
885                "Must be logged into the room to change the " + "availability status.");
886        }
887        // We change the availability status by sending a presence packet to the room with the
888        // new presence status and mode
889        Presence joinPresence = new Presence(Presence.Type.available);
890        joinPresence.setStatus(status);
891        joinPresence.setMode(mode);
892        joinPresence.setTo(room + "/" + nickname);
893
894        // Send join packet.
895        connection.sendStanza(joinPresence);
896    }
897
898    /**
899     * Kicks a visitor or participant from the room. The kicked occupant will receive a presence
900     * of type "unavailable" including a status code 307 and optionally along with the reason
901     * (if provided) and the bare JID of the user who initiated the kick. After the occupant
902     * was kicked from the room, the rest of the occupants will receive a presence of type
903     * "unavailable". The presence will include a status code 307 which means that the occupant
904     * was kicked from the room.
905     *
906     * @param nickname the nickname of the participant or visitor to kick from the room
907     * (e.g. "john").
908     * @param reason the reason why the participant or visitor is being kicked from the room.
909     * @throws XMPPErrorException if an error occurs kicking the occupant. In particular, a
910     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
911     *      was intended to be kicked (i.e. Not Allowed error); or a
912     *      403 error can occur if the occupant that intended to kick another occupant does
913     *      not have kicking privileges (i.e. Forbidden error); or a
914     *      400 error can occur if the provided nickname is not present in the room.
915     * @throws NoResponseException if there was no response from the server.
916     * @throws NotConnectedException 
917     */
918    public void kickParticipant(String nickname, String reason) throws XMPPErrorException, NoResponseException, NotConnectedException {
919        changeRole(nickname, MUCRole.none, reason);
920    }
921
922    /**
923     * Sends a voice request to the MUC. The room moderators usually need to approve this request.
924     *
925     * @throws NotConnectedException
926     * @see <a href="http://xmpp.org/extensions/xep-0045.html#requestvoice">XEP-45 § 7.13 Requesting
927     *      Voice</a>
928     * @since 4.1
929     */
930    public void requestVoice() throws NotConnectedException {
931        DataForm form = new DataForm(DataForm.Type.submit);
932        FormField formTypeField = new FormField(FormField.FORM_TYPE);
933        formTypeField.addValue(MUCInitialPresence.NAMESPACE + "#request");
934        form.addField(formTypeField);
935        FormField requestVoiceField = new FormField("muc#role");
936        requestVoiceField.setType(FormField.Type.text_single);
937        requestVoiceField.setLabel("Requested role");
938        requestVoiceField.addValue("participant");
939        form.addField(requestVoiceField);
940        Message message = new Message(room);
941        message.addExtension(form);
942        connection.sendStanza(message);
943    }
944
945    /**
946     * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
947     * who does and does not have "voice" in the room. To have voice means that a room occupant
948     * is able to send messages to the room occupants.
949     *
950     * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
951     * @throws XMPPErrorException if an error occurs granting voice to a visitor. In particular, a
952     *      403 error can occur if the occupant that intended to grant voice is not
953     *      a moderator in this room (i.e. Forbidden error); or a
954     *      400 error can occur if the provided nickname is not present in the room.
955     * @throws NoResponseException if there was no response from the server.
956     * @throws NotConnectedException 
957     */
958    public void grantVoice(Collection<String> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException {
959        changeRole(nicknames, MUCRole.participant);
960    }
961
962    /**
963     * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
964     * who does and does not have "voice" in the room. To have voice means that a room occupant
965     * is able to send messages to the room occupants.
966     *
967     * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
968     * @throws XMPPErrorException if an error occurs granting voice to a visitor. In particular, a
969     *      403 error can occur if the occupant that intended to grant voice is not
970     *      a moderator in this room (i.e. Forbidden error); or a
971     *      400 error can occur if the provided nickname is not present in the room.
972     * @throws NoResponseException if there was no response from the server.
973     * @throws NotConnectedException 
974     */
975    public void grantVoice(String nickname) throws XMPPErrorException, NoResponseException, NotConnectedException {
976        changeRole(nickname, MUCRole.participant, null);
977    }
978
979    /**
980     * Revokes voice from participants in the room. In a moderated room, a moderator may want to
981     * revoke an occupant's privileges to speak. To have voice means that a room occupant
982     * is able to send messages to the room occupants.
983     *
984     * @param nicknames the nicknames of the participants to revoke voice (e.g. "john").
985     * @throws XMPPErrorException if an error occurs revoking voice from a participant. In particular, a
986     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
987     *      was tried to revoke his voice (i.e. Not Allowed error); or a
988     *      400 error can occur if the provided nickname is not present in the room.
989     * @throws NoResponseException if there was no response from the server.
990     * @throws NotConnectedException 
991     */
992    public void revokeVoice(Collection<String> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException {
993        changeRole(nicknames, MUCRole.visitor);
994    }
995
996    /**
997     * Revokes voice from a participant in the room. In a moderated room, a moderator may want to
998     * revoke an occupant's privileges to speak. To have voice means that a room occupant
999     * is able to send messages to the room occupants.
1000     *
1001     * @param nickname the nickname of the participant to revoke voice (e.g. "john").
1002     * @throws XMPPErrorException if an error occurs revoking voice from a participant. In particular, a
1003     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1004     *      was tried to revoke his voice (i.e. Not Allowed error); or a
1005     *      400 error can occur if the provided nickname is not present in the room.
1006     * @throws NoResponseException if there was no response from the server.
1007     * @throws NotConnectedException 
1008     */
1009    public void revokeVoice(String nickname) throws XMPPErrorException, NoResponseException, NotConnectedException {
1010        changeRole(nickname, MUCRole.visitor, null);
1011    }
1012
1013    /**
1014     * Bans users from the room. An admin or owner of the room can ban users from a room. This
1015     * means that the banned user will no longer be able to join the room unless the ban has been
1016     * removed. If the banned user was present in the room then he/she will be removed from the
1017     * room and notified that he/she was banned along with the reason (if provided) and the bare
1018     * XMPP user ID of the user who initiated the ban.
1019     *
1020     * @param jids the bare XMPP user IDs of the users to ban.
1021     * @throws XMPPErrorException if an error occurs banning a user. In particular, a
1022     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1023     *      was tried to be banned (i.e. Not Allowed error).
1024     * @throws NoResponseException if there was no response from the server.
1025     * @throws NotConnectedException 
1026     */
1027    public void banUsers(Collection<String> jids) throws XMPPErrorException, NoResponseException, NotConnectedException {
1028        changeAffiliationByAdmin(jids, MUCAffiliation.outcast);
1029    }
1030
1031    /**
1032     * Bans a user from the room. An admin or owner of the room can ban users from a room. This
1033     * means that the banned user will no longer be able to join the room unless the ban has been
1034     * removed. If the banned user was present in the room then he/she will be removed from the
1035     * room and notified that he/she was banned along with the reason (if provided) and the bare
1036     * XMPP user ID of the user who initiated the ban.
1037     *
1038     * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org").
1039     * @param reason the optional reason why the user was banned.
1040     * @throws XMPPErrorException if an error occurs banning a user. In particular, a
1041     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1042     *      was tried to be banned (i.e. Not Allowed error).
1043     * @throws NoResponseException if there was no response from the server.
1044     * @throws NotConnectedException 
1045     */
1046    public void banUser(String jid, String reason) throws XMPPErrorException, NoResponseException, NotConnectedException {
1047        changeAffiliationByAdmin(jid, MUCAffiliation.outcast, reason);
1048    }
1049
1050    /**
1051     * Grants membership to other users. Only administrators are able to grant membership. A user
1052     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1053     * that a user cannot enter without being on the member list).
1054     *
1055     * @param jids the XMPP user IDs of the users to grant membership.
1056     * @throws XMPPErrorException if an error occurs granting membership to a user.
1057     * @throws NoResponseException if there was no response from the server.
1058     * @throws NotConnectedException 
1059     */
1060    public void grantMembership(Collection<String> jids) throws XMPPErrorException, NoResponseException, NotConnectedException {
1061        changeAffiliationByAdmin(jids, MUCAffiliation.member);
1062    }
1063
1064    /**
1065     * Grants membership to a user. Only administrators are able to grant membership. A user
1066     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1067     * that a user cannot enter without being on the member list).
1068     *
1069     * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org").
1070     * @throws XMPPErrorException if an error occurs granting membership to a user.
1071     * @throws NoResponseException if there was no response from the server.
1072     * @throws NotConnectedException 
1073     */
1074    public void grantMembership(String jid) throws XMPPErrorException, NoResponseException, NotConnectedException {
1075        changeAffiliationByAdmin(jid, MUCAffiliation.member, null);
1076    }
1077
1078    /**
1079     * Revokes users' membership. Only administrators are able to revoke membership. A user
1080     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1081     * that a user cannot enter without being on the member list). If the user is in the room and
1082     * the room is of type members-only then the user will be removed from the room.
1083     *
1084     * @param jids the bare XMPP user IDs of the users to revoke membership.
1085     * @throws XMPPErrorException if an error occurs revoking membership to a user.
1086     * @throws NoResponseException if there was no response from the server.
1087     * @throws NotConnectedException 
1088     */
1089    public void revokeMembership(Collection<String> jids) throws XMPPErrorException, NoResponseException, NotConnectedException {
1090        changeAffiliationByAdmin(jids, MUCAffiliation.none);
1091    }
1092
1093    /**
1094     * Revokes a user's membership. Only administrators are able to revoke membership. A user
1095     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1096     * that a user cannot enter without being on the member list). If the user is in the room and
1097     * the room is of type members-only then the user will be removed from the room.
1098     *
1099     * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org").
1100     * @throws XMPPErrorException if an error occurs revoking membership to a user.
1101     * @throws NoResponseException if there was no response from the server.
1102     * @throws NotConnectedException 
1103     */
1104    public void revokeMembership(String jid) throws XMPPErrorException, NoResponseException, NotConnectedException {
1105        changeAffiliationByAdmin(jid, MUCAffiliation.none, null);
1106    }
1107
1108    /**
1109     * Grants moderator privileges to participants or visitors. Room administrators may grant
1110     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
1111     * other users, modify room's subject plus all the partcipants privileges.
1112     *
1113     * @param nicknames the nicknames of the occupants to grant moderator privileges.
1114     * @throws XMPPErrorException if an error occurs granting moderator privileges to a user.
1115     * @throws NoResponseException if there was no response from the server.
1116     * @throws NotConnectedException 
1117     */
1118    public void grantModerator(Collection<String> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException {
1119        changeRole(nicknames, MUCRole.moderator);
1120    }
1121
1122    /**
1123     * Grants moderator privileges to a participant or visitor. Room administrators may grant
1124     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
1125     * other users, modify room's subject plus all the partcipants privileges.
1126     *
1127     * @param nickname the nickname of the occupant to grant moderator privileges.
1128     * @throws XMPPErrorException if an error occurs granting moderator privileges to a user.
1129     * @throws NoResponseException if there was no response from the server.
1130     * @throws NotConnectedException 
1131     */
1132    public void grantModerator(String nickname) throws XMPPErrorException, NoResponseException, NotConnectedException {
1133        changeRole(nickname, MUCRole.moderator, null);
1134    }
1135
1136    /**
1137     * Revokes moderator privileges from other users. The occupant that loses moderator
1138     * privileges will become a participant. Room administrators may revoke moderator privileges
1139     * only to occupants whose affiliation is member or none. This means that an administrator is
1140     * not allowed to revoke moderator privileges from other room administrators or owners.
1141     *
1142     * @param nicknames the nicknames of the occupants to revoke moderator privileges.
1143     * @throws XMPPErrorException if an error occurs revoking moderator privileges from a user.
1144     * @throws NoResponseException if there was no response from the server.
1145     * @throws NotConnectedException 
1146     */
1147    public void revokeModerator(Collection<String> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException {
1148        changeRole(nicknames, MUCRole.participant);
1149    }
1150
1151    /**
1152     * Revokes moderator privileges from another user. The occupant that loses moderator
1153     * privileges will become a participant. Room administrators may revoke moderator privileges
1154     * only to occupants whose affiliation is member or none. This means that an administrator is
1155     * not allowed to revoke moderator privileges from other room administrators or owners.
1156     *
1157     * @param nickname the nickname of the occupant to revoke moderator privileges.
1158     * @throws XMPPErrorException if an error occurs revoking moderator privileges from a user.
1159     * @throws NoResponseException if there was no response from the server.
1160     * @throws NotConnectedException 
1161     */
1162    public void revokeModerator(String nickname) throws XMPPErrorException, NoResponseException, NotConnectedException {
1163        changeRole(nickname, MUCRole.participant, null);
1164    }
1165
1166    /**
1167     * Grants ownership privileges to other users. Room owners may grant ownership privileges.
1168     * Some room implementations will not allow to grant ownership privileges to other users.
1169     * An owner is allowed to change defining room features as well as perform all administrative
1170     * functions.
1171     *
1172     * @param jids the collection of bare XMPP user IDs of the users to grant ownership.
1173     * @throws XMPPErrorException if an error occurs granting ownership privileges to a user.
1174     * @throws NoResponseException if there was no response from the server.
1175     * @throws NotConnectedException 
1176     */
1177    public void grantOwnership(Collection<String> jids) throws XMPPErrorException, NoResponseException, NotConnectedException {
1178        changeAffiliationByAdmin(jids, MUCAffiliation.owner);
1179    }
1180
1181    /**
1182     * Grants ownership privileges to another user. Room owners may grant ownership privileges.
1183     * Some room implementations will not allow to grant ownership privileges to other users.
1184     * An owner is allowed to change defining room features as well as perform all administrative
1185     * functions.
1186     *
1187     * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org").
1188     * @throws XMPPErrorException if an error occurs granting ownership privileges to a user.
1189     * @throws NoResponseException if there was no response from the server.
1190     * @throws NotConnectedException 
1191     */
1192    public void grantOwnership(String jid) throws XMPPErrorException, NoResponseException, NotConnectedException {
1193        changeAffiliationByAdmin(jid, MUCAffiliation.owner, null);
1194    }
1195
1196    /**
1197     * Revokes ownership privileges from other users. The occupant that loses ownership
1198     * privileges will become an administrator. Room owners may revoke ownership privileges.
1199     * Some room implementations will not allow to grant ownership privileges to other users.
1200     *
1201     * @param jids the bare XMPP user IDs of the users to revoke ownership.
1202     * @throws XMPPErrorException if an error occurs revoking ownership privileges from a user.
1203     * @throws NoResponseException if there was no response from the server.
1204     * @throws NotConnectedException 
1205     */
1206    public void revokeOwnership(Collection<String> jids) throws XMPPErrorException, NoResponseException, NotConnectedException {
1207        changeAffiliationByAdmin(jids, MUCAffiliation.admin);
1208    }
1209
1210    /**
1211     * Revokes ownership privileges from another user. The occupant that loses ownership
1212     * privileges will become an administrator. Room owners may revoke ownership privileges.
1213     * Some room implementations will not allow to grant ownership privileges to other users.
1214     *
1215     * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org").
1216     * @throws XMPPErrorException if an error occurs revoking ownership privileges from a user.
1217     * @throws NoResponseException if there was no response from the server.
1218     * @throws NotConnectedException 
1219     */
1220    public void revokeOwnership(String jid) throws XMPPErrorException, NoResponseException, NotConnectedException {
1221        changeAffiliationByAdmin(jid, MUCAffiliation.admin, null);
1222    }
1223
1224    /**
1225     * Grants administrator privileges to other users. Room owners may grant administrator
1226     * privileges to a member or unaffiliated user. An administrator is allowed to perform
1227     * administrative functions such as banning users and edit moderator list.
1228     *
1229     * @param jids the bare XMPP user IDs of the users to grant administrator privileges.
1230     * @throws XMPPErrorException if an error occurs granting administrator privileges to a user.
1231     * @throws NoResponseException if there was no response from the server.
1232     * @throws NotConnectedException 
1233     */
1234    public void grantAdmin(Collection<String> jids) throws XMPPErrorException, NoResponseException, NotConnectedException {
1235        changeAffiliationByAdmin(jids, MUCAffiliation.admin);
1236    }
1237
1238    /**
1239     * Grants administrator privileges to another user. Room owners may grant administrator
1240     * privileges to a member or unaffiliated user. An administrator is allowed to perform
1241     * administrative functions such as banning users and edit moderator list.
1242     *
1243     * @param jid the bare XMPP user ID of the user to grant administrator privileges
1244     * (e.g. "user@host.org").
1245     * @throws XMPPErrorException if an error occurs granting administrator privileges to a user.
1246     * @throws NoResponseException if there was no response from the server.
1247     * @throws NotConnectedException 
1248     */
1249    public void grantAdmin(String jid) throws XMPPErrorException, NoResponseException, NotConnectedException {
1250        changeAffiliationByAdmin(jid, MUCAffiliation.admin);
1251    }
1252
1253    /**
1254     * Revokes administrator privileges from users. The occupant that loses administrator
1255     * privileges will become a member. Room owners may revoke administrator privileges from
1256     * a member or unaffiliated user.
1257     *
1258     * @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
1259     * @throws XMPPErrorException if an error occurs revoking administrator privileges from a user.
1260     * @throws NoResponseException if there was no response from the server.
1261     * @throws NotConnectedException 
1262     */
1263    public void revokeAdmin(Collection<String> jids) throws XMPPErrorException, NoResponseException, NotConnectedException {
1264        changeAffiliationByAdmin(jids, MUCAffiliation.admin);
1265    }
1266
1267    /**
1268     * Revokes administrator privileges from a user. The occupant that loses administrator
1269     * privileges will become a member. Room owners may revoke administrator privileges from
1270     * a member or unaffiliated user.
1271     *
1272     * @param jid the bare XMPP user ID of the user to revoke administrator privileges
1273     * (e.g. "user@host.org").
1274     * @throws XMPPErrorException if an error occurs revoking administrator privileges from a user.
1275     * @throws NoResponseException if there was no response from the server.
1276     * @throws NotConnectedException 
1277     */
1278    public void revokeAdmin(String jid) throws XMPPErrorException, NoResponseException, NotConnectedException {
1279        changeAffiliationByAdmin(jid, MUCAffiliation.member);
1280    }
1281
1282    /**
1283     * Tries to change the affiliation with an 'muc#admin' namespace
1284     *
1285     * @param jid
1286     * @param affiliation
1287     * @throws XMPPErrorException
1288     * @throws NoResponseException
1289     * @throws NotConnectedException
1290     */
1291    private void changeAffiliationByAdmin(String jid, MUCAffiliation affiliation)
1292                    throws NoResponseException, XMPPErrorException,
1293                    NotConnectedException {
1294        changeAffiliationByAdmin(jid, affiliation, null);
1295    }
1296
1297    /**
1298     * Tries to change the affiliation with an 'muc#admin' namespace
1299     *
1300     * @param jid
1301     * @param affiliation
1302     * @param reason the reason for the affiliation change (optional)
1303     * @throws XMPPErrorException 
1304     * @throws NoResponseException 
1305     * @throws NotConnectedException 
1306     */
1307    private void changeAffiliationByAdmin(String jid, MUCAffiliation affiliation, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException
1308            {
1309        MUCAdmin iq = new MUCAdmin();
1310        iq.setTo(room);
1311        iq.setType(IQ.Type.set);
1312        // Set the new affiliation.
1313        MUCItem item = new MUCItem(affiliation, jid, reason);
1314        iq.addItem(item);
1315
1316        connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
1317    }
1318
1319    private void changeAffiliationByAdmin(Collection<String> jids, MUCAffiliation affiliation)
1320                    throws NoResponseException, XMPPErrorException, NotConnectedException {
1321        MUCAdmin iq = new MUCAdmin();
1322        iq.setTo(room);
1323        iq.setType(IQ.Type.set);
1324        for (String jid : jids) {
1325            // Set the new affiliation.
1326            MUCItem item = new MUCItem(affiliation, jid);
1327            iq.addItem(item);
1328        }
1329
1330        connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
1331    }
1332
1333    private void changeRole(String nickname, MUCRole role, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException {
1334        MUCAdmin iq = new MUCAdmin();
1335        iq.setTo(room);
1336        iq.setType(IQ.Type.set);
1337        // Set the new role.
1338        MUCItem item = new MUCItem(role, nickname, reason);
1339        iq.addItem(item);
1340
1341        connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
1342    }
1343
1344    private void changeRole(Collection<String> nicknames, MUCRole role) throws NoResponseException, XMPPErrorException, NotConnectedException  {
1345        MUCAdmin iq = new MUCAdmin();
1346        iq.setTo(room);
1347        iq.setType(IQ.Type.set);
1348        for (String nickname : nicknames) {
1349            // Set the new role.
1350            MUCItem item = new MUCItem(role, nickname);
1351            iq.addItem(item);
1352        }
1353
1354        connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
1355    }
1356
1357    /**
1358     * Returns the number of occupants in the group chat.<p>
1359     *
1360     * Note: this value will only be accurate after joining the group chat, and
1361     * may fluctuate over time. If you query this value directly after joining the
1362     * group chat it may not be accurate, as it takes a certain amount of time for
1363     * the server to send all presence packets to this client.
1364     *
1365     * @return the number of occupants in the group chat.
1366     */
1367    public int getOccupantsCount() {
1368        return occupantsMap.size();
1369    }
1370
1371    /**
1372     * Returns an Iterator (of Strings) for the list of fully qualified occupants
1373     * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
1374     * Typically, a client would only display the nickname of the occupant. To
1375     * get the nickname from the fully qualified name, use the
1376     * {@link org.jxmpp.util.XmppStringUtils#parseResource(String)} method.
1377     * Note: this value will only be accurate after joining the group chat, and may
1378     * fluctuate over time.
1379     *
1380     * @return a List of the occupants in the group chat.
1381     */
1382    public List<String> getOccupants() {
1383        return Collections.unmodifiableList(new ArrayList<String>(occupantsMap.keySet()));
1384    }
1385
1386    /**
1387     * Returns the presence info for a particular user, or <tt>null</tt> if the user
1388     * is not in the room.<p>
1389     *
1390     * @param user the room occupant to search for his presence. The format of user must
1391     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
1392     * @return the occupant's current presence, or <tt>null</tt> if the user is unavailable
1393     *      or if no presence information is available.
1394     */
1395    public Presence getOccupantPresence(String user) {
1396        return occupantsMap.get(user);
1397    }
1398
1399    /**
1400     * Returns the Occupant information for a particular occupant, or <tt>null</tt> if the
1401     * user is not in the room. The Occupant object may include information such as full
1402     * JID of the user as well as the role and affiliation of the user in the room.<p>
1403     *
1404     * @param user the room occupant to search for his presence. The format of user must
1405     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
1406     * @return the Occupant or <tt>null</tt> if the user is unavailable (i.e. not in the room).
1407     */
1408    public Occupant getOccupant(String user) {
1409        Presence presence = occupantsMap.get(user);
1410        if (presence != null) {
1411            return new Occupant(presence);
1412        }
1413        return null;
1414    }
1415
1416    /**
1417     * Adds a stanza(/packet) listener that will be notified of any new Presence packets
1418     * sent to the group chat. Using a listener is a suitable way to know when the list
1419     * of occupants should be re-loaded due to any changes.
1420     *
1421     * @param listener a stanza(/packet) listener that will be notified of any presence packets
1422     *      sent to the group chat.
1423     * @return true if the listener was not already added.
1424     */
1425    public boolean addParticipantListener(PresenceListener listener) {
1426        return presenceListeners.add(listener);
1427    }
1428
1429    /**
1430     * Removes a stanza(/packet) listener that was being notified of any new Presence packets
1431     * sent to the group chat.
1432     *
1433     * @param listener a stanza(/packet) listener that was being notified of any presence packets
1434     *      sent to the group chat.
1435     * @return true if the listener was removed, otherwise the listener was not added previously.
1436     */
1437    public boolean removeParticipantListener(PresenceListener listener) {
1438        return presenceListeners.remove(listener);
1439    }
1440
1441    /**
1442     * Returns a list of <code>Affiliate</code> with the room owners.
1443     *
1444     * @return a list of <code>Affiliate</code> with the room owners.
1445     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1446     * @throws NoResponseException if there was no response from the server.
1447     * @throws NotConnectedException 
1448     */
1449    public List<Affiliate> getOwners() throws NoResponseException, XMPPErrorException, NotConnectedException {
1450        return getAffiliatesByAdmin(MUCAffiliation.owner);
1451    }
1452
1453    /**
1454     * Returns a list of <code>Affiliate</code> with the room administrators.
1455     *
1456     * @return a list of <code>Affiliate</code> with the room administrators.
1457     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1458     * @throws NoResponseException if there was no response from the server.
1459     * @throws NotConnectedException 
1460     */
1461    public List<Affiliate> getAdmins() throws NoResponseException, XMPPErrorException, NotConnectedException {
1462        return getAffiliatesByAdmin(MUCAffiliation.admin);
1463    }
1464
1465    /**
1466     * Returns a list of <code>Affiliate</code> with the room members.
1467     *
1468     * @return a list of <code>Affiliate</code> with the room members.
1469     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1470     * @throws NoResponseException if there was no response from the server.
1471     * @throws NotConnectedException 
1472     */
1473    public List<Affiliate> getMembers() throws NoResponseException, XMPPErrorException, NotConnectedException  {
1474        return getAffiliatesByAdmin(MUCAffiliation.member);
1475    }
1476
1477    /**
1478     * Returns a list of <code>Affiliate</code> with the room outcasts.
1479     *
1480     * @return a list of <code>Affiliate</code> with the room outcasts.
1481     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1482     * @throws NoResponseException if there was no response from the server.
1483     * @throws NotConnectedException 
1484     */
1485    public List<Affiliate> getOutcasts() throws NoResponseException, XMPPErrorException, NotConnectedException {
1486        return getAffiliatesByAdmin(MUCAffiliation.outcast);
1487    }
1488
1489    /**
1490     * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
1491     * sending a request in the admin namespace.
1492     *
1493     * @param affiliation the affiliation of the users in the room.
1494     * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
1495     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1496     * @throws NoResponseException if there was no response from the server.
1497     * @throws NotConnectedException 
1498     */
1499    private List<Affiliate> getAffiliatesByAdmin(MUCAffiliation affiliation) throws NoResponseException, XMPPErrorException, NotConnectedException {
1500        MUCAdmin iq = new MUCAdmin();
1501        iq.setTo(room);
1502        iq.setType(IQ.Type.get);
1503        // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
1504        MUCItem item = new MUCItem(affiliation);
1505        iq.addItem(item);
1506
1507        MUCAdmin answer = (MUCAdmin) connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
1508
1509        // Get the list of affiliates from the server's answer
1510        List<Affiliate> affiliates = new ArrayList<Affiliate>();
1511        for (MUCItem mucadminItem : answer.getItems()) {
1512            affiliates.add(new Affiliate(mucadminItem));
1513        }
1514        return affiliates;
1515    }
1516
1517    /**
1518     * Returns a list of <code>Occupant</code> with the room moderators.
1519     *
1520     * @return a list of <code>Occupant</code> with the room moderators.
1521     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1522     * @throws NoResponseException if there was no response from the server.
1523     * @throws NotConnectedException 
1524     */
1525    public List<Occupant> getModerators() throws NoResponseException, XMPPErrorException, NotConnectedException {
1526        return getOccupants(MUCRole.moderator);
1527    }
1528
1529    /**
1530     * Returns a list of <code>Occupant</code> with the room participants.
1531     * 
1532     * @return a list of <code>Occupant</code> with the room participants.
1533     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1534     * @throws NoResponseException if there was no response from the server.
1535     * @throws NotConnectedException 
1536     */
1537    public List<Occupant> getParticipants() throws NoResponseException, XMPPErrorException, NotConnectedException {
1538        return getOccupants(MUCRole.participant);
1539    }
1540
1541    /**
1542     * Returns a list of <code>Occupant</code> that have the specified room role.
1543     *
1544     * @param role the role of the occupant in the room.
1545     * @return a list of <code>Occupant</code> that have the specified room role.
1546     * @throws XMPPErrorException if an error occured while performing the request to the server or you
1547     *         don't have enough privileges to get this information.
1548     * @throws NoResponseException if there was no response from the server.
1549     * @throws NotConnectedException 
1550     */
1551    private List<Occupant> getOccupants(MUCRole role) throws NoResponseException, XMPPErrorException, NotConnectedException {
1552        MUCAdmin iq = new MUCAdmin();
1553        iq.setTo(room);
1554        iq.setType(IQ.Type.get);
1555        // Set the specified role. This may request the list of moderators/participants.
1556        MUCItem item = new MUCItem(role);
1557        iq.addItem(item);
1558
1559        MUCAdmin answer = (MUCAdmin) connection.createPacketCollectorAndSend(iq).nextResultOrThrow();
1560        // Get the list of participants from the server's answer
1561        List<Occupant> participants = new ArrayList<Occupant>();
1562        for (MUCItem mucadminItem : answer.getItems()) {
1563            participants.add(new Occupant(mucadminItem));
1564        }
1565        return participants;
1566    }
1567
1568    /**
1569     * Sends a message to the chat room.
1570     *
1571     * @param text the text of the message to send.
1572     * @throws NotConnectedException 
1573     */
1574    public void sendMessage(String text) throws NotConnectedException {
1575        Message message = createMessage();
1576        message.setBody(text);
1577        connection.sendStanza(message);
1578    }
1579
1580    /**
1581     * Returns a new Chat for sending private messages to a given room occupant.
1582     * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
1583     * service will change the 'from' address to the sender's room JID and delivering the message
1584     * to the intended recipient's full JID.
1585     *
1586     * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul').
1587     * @param listener the listener is a message listener that will handle messages for the newly
1588     * created chat.
1589     * @return new Chat for sending private messages to a given room occupant.
1590     */
1591    public Chat createPrivateChat(String occupant, ChatMessageListener listener) {
1592        return ChatManager.getInstanceFor(connection).createChat(occupant, listener);
1593    }
1594
1595    /**
1596     * Creates a new Message to send to the chat room.
1597     *
1598     * @return a new Message addressed to the chat room.
1599     */
1600    public Message createMessage() {
1601        return new Message(room, Message.Type.groupchat);
1602    }
1603
1604    /**
1605     * Sends a Message to the chat room.
1606     *
1607     * @param message the message.
1608     * @throws NotConnectedException 
1609     */
1610    public void sendMessage(Message message) throws NotConnectedException {
1611        message.setTo(room);
1612        message.setType(Message.Type.groupchat);
1613        connection.sendStanza(message);
1614    }
1615
1616    /**
1617    * Polls for and returns the next message, or <tt>null</tt> if there isn't
1618    * a message immediately available. This method provides significantly different
1619    * functionalty than the {@link #nextMessage()} method since it's non-blocking.
1620    * In other words, the method call will always return immediately, whereas the
1621    * nextMessage method will return only when a message is available (or after
1622    * a specific timeout).
1623    *
1624    * @return the next message if one is immediately available and
1625    *      <tt>null</tt> otherwise.
1626     * @throws MUCNotJoinedException 
1627    */
1628    public Message pollMessage() throws MUCNotJoinedException {
1629        if (messageCollector == null) {
1630            throw new MUCNotJoinedException(this);
1631        }
1632        return messageCollector.pollResult();
1633    }
1634
1635    /**
1636     * Returns the next available message in the chat. The method call will block
1637     * (not return) until a message is available.
1638     *
1639     * @return the next message.
1640     * @throws MUCNotJoinedException 
1641     */
1642    public Message nextMessage() throws MUCNotJoinedException {
1643        if (messageCollector == null) {
1644            throw new MUCNotJoinedException(this);
1645        }
1646        return  messageCollector.nextResult();
1647    }
1648
1649    /**
1650     * Returns the next available message in the chat. The method call will block
1651     * (not return) until a stanza(/packet) is available or the <tt>timeout</tt> has elapased.
1652     * If the timeout elapses without a result, <tt>null</tt> will be returned.
1653     *
1654     * @param timeout the maximum amount of time to wait for the next message.
1655     * @return the next message, or <tt>null</tt> if the timeout elapses without a
1656     *      message becoming available.
1657     * @throws MUCNotJoinedException 
1658     */
1659    public Message nextMessage(long timeout) throws MUCNotJoinedException {
1660        if (messageCollector == null) {
1661            throw new MUCNotJoinedException(this);
1662        }
1663        return messageCollector.nextResult(timeout);
1664    }
1665
1666    /**
1667     * Adds a stanza(/packet) listener that will be notified of any new messages in the
1668     * group chat. Only "group chat" messages addressed to this group chat will
1669     * be delivered to the listener. If you wish to listen for other packets
1670     * that may be associated with this group chat, you should register a
1671     * PacketListener directly with the XMPPConnection with the appropriate
1672     * PacketListener.
1673     *
1674     * @param listener a stanza(/packet) listener.
1675     * @return true if the listener was not already added.
1676     */
1677    public boolean addMessageListener(MessageListener listener) {
1678        return messageListeners.add(listener);
1679    }
1680
1681    /**
1682     * Removes a stanza(/packet) listener that was being notified of any new messages in the
1683     * multi user chat. Only "group chat" messages addressed to this multi user chat were
1684     * being delivered to the listener.
1685     *
1686     * @param listener a stanza(/packet) listener.
1687     * @return true if the listener was removed, otherwise the listener was not added previously.
1688     */
1689    public boolean removeMessageListener(MessageListener listener) {
1690        return messageListeners.remove(listener);
1691    }
1692
1693    /**
1694     * Changes the subject within the room. As a default, only users with a role of "moderator"
1695     * are allowed to change the subject in a room. Although some rooms may be configured to
1696     * allow a mere participant or even a visitor to change the subject.
1697     *
1698     * @param subject the new room's subject to set.
1699     * @throws XMPPErrorException if someone without appropriate privileges attempts to change the
1700     *          room subject will throw an error with code 403 (i.e. Forbidden)
1701     * @throws NoResponseException if there was no response from the server.
1702     * @throws NotConnectedException 
1703     */
1704    public void changeSubject(final String subject) throws NoResponseException, XMPPErrorException, NotConnectedException {
1705        Message message = createMessage();
1706        message.setSubject(subject);
1707        // Wait for an error or confirmation message back from the server.
1708        StanzaFilter responseFilter = new AndFilter(fromRoomGroupchatFilter, new StanzaFilter() {
1709            @Override
1710            public boolean accept(Stanza packet) {
1711                Message msg = (Message) packet;
1712                return subject.equals(msg.getSubject());
1713            }
1714        });
1715        PacketCollector response = connection.createPacketCollectorAndSend(responseFilter, message);
1716        // Wait up to a certain number of seconds for a reply.
1717        response.nextResultOrThrow();
1718    }
1719
1720    /**
1721     * Remove the connection callbacks (PacketListener, PacketInterceptor, PacketCollector) used by this MUC from the
1722     * connection.
1723     */
1724    private void removeConnectionCallbacks() {
1725        connection.removeSyncStanzaListener(messageListener);
1726        connection.removeSyncStanzaListener(presenceListener);
1727        connection.removeSyncStanzaListener(declinesListener);
1728        connection.removePacketInterceptor(presenceInterceptor);
1729        if (messageCollector != null) {
1730            messageCollector.cancel();
1731            messageCollector = null;
1732        }
1733    }
1734
1735    /**
1736     * Remove all callbacks and resources necessary when the user has left the room for some reason.
1737     */
1738    private synchronized void userHasLeft() {
1739        // Update the list of joined rooms
1740        multiUserChatManager.removeJoinedRoom(room);
1741        removeConnectionCallbacks();
1742    }
1743
1744    /**
1745     * Adds a listener that will be notified of changes in your status in the room
1746     * such as the user being kicked, banned, or granted admin permissions.
1747     *
1748     * @param listener a user status listener.
1749     * @return true if the user status listener was not already added.
1750     */
1751    public boolean addUserStatusListener(UserStatusListener listener) {
1752        return userStatusListeners.add(listener);
1753    }
1754
1755    /**
1756     * Removes a listener that was being notified of changes in your status in the room
1757     * such as the user being kicked, banned, or granted admin permissions.
1758     *
1759     * @param listener a user status listener.
1760     * @return true if the listener was registered and is now removed.
1761     */
1762    public boolean removeUserStatusListener(UserStatusListener listener) {
1763        return userStatusListeners.remove(listener);
1764    }
1765
1766    /**
1767     * Adds a listener that will be notified of changes in occupants status in the room
1768     * such as the user being kicked, banned, or granted admin permissions.
1769     *
1770     * @param listener a participant status listener.
1771     * @return true if the listener was not already added.
1772     */
1773    public boolean addParticipantStatusListener(ParticipantStatusListener listener) {
1774        return participantStatusListeners.add(listener);
1775    }
1776
1777    /**
1778     * Removes a listener that was being notified of changes in occupants status in the room
1779     * such as the user being kicked, banned, or granted admin permissions.
1780     *
1781     * @param listener a participant status listener.
1782     * @return true if the listener was registered and is now removed.
1783     */
1784    public boolean removeParticipantStatusListener(ParticipantStatusListener listener) {
1785        return participantStatusListeners.remove(listener);
1786    }
1787
1788    /**
1789     * Fires notification events if the role of a room occupant has changed. If the occupant that
1790     * changed his role is your occupant then the <code>UserStatusListeners</code> added to this
1791     * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
1792     * his role is not yours then the <code>ParticipantStatusListeners</code> added to this
1793     * <code>MultiUserChat</code> will be fired. The following table shows the events that will
1794     * be fired depending on the previous and new role of the occupant.
1795     *
1796     * <pre>
1797     * <table border="1">
1798     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
1799     *
1800     * <tr><td>None</td><td>Visitor</td><td>--</td></tr>
1801     * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
1802     * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
1803     *
1804     * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
1805     * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
1806     * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
1807     *
1808     * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
1809     * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
1810     * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
1811     *
1812     * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
1813     * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
1814     * <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
1815     * </table>
1816     * </pre>
1817     *
1818     * @param oldRole the previous role of the user in the room before receiving the new presence
1819     * @param newRole the new role of the user in the room after receiving the new presence
1820     * @param isUserModification whether the received presence is about your user in the room or not
1821     * @param from the occupant whose role in the room has changed
1822     * (e.g. room@conference.jabber.org/nick).
1823     */
1824    private void checkRoleModifications(
1825        MUCRole oldRole,
1826        MUCRole newRole,
1827        boolean isUserModification,
1828        String from) {
1829        // Voice was granted to a visitor
1830        if (("visitor".equals(oldRole) || "none".equals(oldRole))
1831            && "participant".equals(newRole)) {
1832            if (isUserModification) {
1833                for (UserStatusListener listener : userStatusListeners) {
1834                    listener.voiceGranted();
1835                }
1836            }
1837            else {
1838                for (ParticipantStatusListener listener : participantStatusListeners) {
1839                    listener.voiceGranted(from);
1840                }
1841            }
1842        }
1843        // The participant's voice was revoked from the room
1844        else if (
1845            "participant".equals(oldRole)
1846                && ("visitor".equals(newRole) || "none".equals(newRole))) {
1847            if (isUserModification) {
1848                for (UserStatusListener listener : userStatusListeners) {
1849                    listener.voiceRevoked();
1850                }
1851            }
1852            else {
1853                for (ParticipantStatusListener listener : participantStatusListeners) {
1854                    listener.voiceRevoked(from);
1855                }
1856            }
1857        }
1858        // Moderator privileges were granted to a participant
1859        if (!"moderator".equals(oldRole) && "moderator".equals(newRole)) {
1860            if ("visitor".equals(oldRole) || "none".equals(oldRole)) {
1861                if (isUserModification) {
1862                    for (UserStatusListener listener : userStatusListeners) {
1863                        listener.voiceGranted();
1864                    }
1865                }
1866                else {
1867                    for (ParticipantStatusListener listener : participantStatusListeners) {
1868                        listener.voiceGranted(from);
1869                    }
1870                }
1871            }
1872            if (isUserModification) {
1873                for (UserStatusListener listener : userStatusListeners) {
1874                    listener.moderatorGranted();
1875                }
1876            }
1877            else {
1878                for (ParticipantStatusListener listener : participantStatusListeners) {
1879                    listener.moderatorGranted(from);
1880                }
1881            }
1882        }
1883        // Moderator privileges were revoked from a participant
1884        else if ("moderator".equals(oldRole) && !"moderator".equals(newRole)) {
1885            if ("visitor".equals(newRole) || "none".equals(newRole)) {
1886                if (isUserModification) {
1887                    for (UserStatusListener listener : userStatusListeners) {
1888                        listener.voiceRevoked();
1889                    }
1890                }
1891                else {
1892                    for (ParticipantStatusListener listener : participantStatusListeners) {
1893                        listener.voiceRevoked(from);
1894                    }
1895                }
1896            }
1897            if (isUserModification) {
1898                for (UserStatusListener listener : userStatusListeners) {
1899                    listener.moderatorRevoked();
1900                }
1901            }
1902            else {
1903                for (ParticipantStatusListener listener : participantStatusListeners) {
1904                    listener.moderatorRevoked(from);
1905                }
1906            }
1907        }
1908    }
1909
1910    /**
1911     * Fires notification events if the affiliation of a room occupant has changed. If the
1912     * occupant that changed his affiliation is your occupant then the
1913     * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
1914     * On the other hand, if the occupant that changed his affiliation is not yours then the
1915     * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
1916     * fired. The following table shows the events that will be fired depending on the previous
1917     * and new affiliation of the occupant.
1918     *
1919     * <pre>
1920     * <table border="1">
1921     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
1922     *
1923     * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
1924     * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
1925     * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
1926     *
1927     * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
1928     * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
1929     * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
1930     *
1931     * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
1932     * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
1933     * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
1934     *
1935     * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
1936     * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
1937     * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
1938     * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
1939     * </table>
1940     * </pre>
1941     *
1942     * @param oldAffiliation the previous affiliation of the user in the room before receiving the
1943     * new presence
1944     * @param newAffiliation the new affiliation of the user in the room after receiving the new
1945     * presence
1946     * @param isUserModification whether the received presence is about your user in the room or not
1947     * @param from the occupant whose role in the room has changed
1948     * (e.g. room@conference.jabber.org/nick).
1949     */
1950    private void checkAffiliationModifications(
1951        MUCAffiliation oldAffiliation,
1952        MUCAffiliation newAffiliation,
1953        boolean isUserModification,
1954        String from) {
1955        // First check for revoked affiliation and then for granted affiliations. The idea is to
1956        // first fire the "revoke" events and then fire the "grant" events.
1957
1958        // The user's ownership to the room was revoked
1959        if ("owner".equals(oldAffiliation) && !"owner".equals(newAffiliation)) {
1960            if (isUserModification) {
1961                for (UserStatusListener listener : userStatusListeners) {
1962                    listener.ownershipRevoked();
1963                }
1964            }
1965            else {
1966                for (ParticipantStatusListener listener : participantStatusListeners) {
1967                    listener.ownershipRevoked(from);
1968                }
1969            }
1970        }
1971        // The user's administrative privileges to the room were revoked
1972        else if ("admin".equals(oldAffiliation) && !"admin".equals(newAffiliation)) {
1973            if (isUserModification) {
1974                for (UserStatusListener listener : userStatusListeners) {
1975                    listener.adminRevoked();
1976                }
1977            }
1978            else {
1979                for (ParticipantStatusListener listener : participantStatusListeners) {
1980                    listener.adminRevoked(from);
1981                }
1982            }
1983        }
1984        // The user's membership to the room was revoked
1985        else if ("member".equals(oldAffiliation) && !"member".equals(newAffiliation)) {
1986            if (isUserModification) {
1987                for (UserStatusListener listener : userStatusListeners) {
1988                    listener.membershipRevoked();
1989                }
1990            }
1991            else {
1992                for (ParticipantStatusListener listener : participantStatusListeners) {
1993                    listener.membershipRevoked(from);
1994                }
1995            }
1996        }
1997
1998        // The user was granted ownership to the room
1999        if (!"owner".equals(oldAffiliation) && "owner".equals(newAffiliation)) {
2000            if (isUserModification) {
2001                for (UserStatusListener listener : userStatusListeners) {
2002                    listener.ownershipGranted();
2003                }
2004            }
2005            else {
2006                for (ParticipantStatusListener listener : participantStatusListeners) {
2007                    listener.ownershipGranted(from);
2008                }
2009            }
2010        }
2011        // The user was granted administrative privileges to the room
2012        else if (!"admin".equals(oldAffiliation) && "admin".equals(newAffiliation)) {
2013            if (isUserModification) {
2014                for (UserStatusListener listener : userStatusListeners) {
2015                    listener.adminGranted();
2016                }
2017            }
2018            else {
2019                for (ParticipantStatusListener listener : participantStatusListeners) {
2020                    listener.adminGranted(from);
2021                }
2022            }
2023        }
2024        // The user was granted membership to the room
2025        else if (!"member".equals(oldAffiliation) && "member".equals(newAffiliation)) {
2026            if (isUserModification) {
2027                for (UserStatusListener listener : userStatusListeners) {
2028                    listener.membershipGranted();
2029                }
2030            }
2031            else {
2032                for (ParticipantStatusListener listener : participantStatusListeners) {
2033                    listener.membershipGranted(from);
2034                }
2035            }
2036        }
2037    }
2038
2039    /**
2040     * Fires events according to the received presence code.
2041     *
2042     * @param statusCodes
2043     * @param isUserModification
2044     * @param mucUser
2045     * @param from
2046     */
2047    private void checkPresenceCode(
2048        Set<Status> statusCodes,
2049        boolean isUserModification,
2050        MUCUser mucUser,
2051        String from) {
2052        // Check if an occupant was kicked from the room
2053        if (statusCodes.contains(Status.KICKED_307)) {
2054            // Check if this occupant was kicked
2055            if (isUserModification) {
2056                joined = false;
2057                for (UserStatusListener listener : userStatusListeners) {
2058                    listener.kicked(mucUser.getItem().getActor(), mucUser.getItem().getReason());
2059                }
2060
2061                // Reset occupant information.
2062                occupantsMap.clear();
2063                nickname = null;
2064                userHasLeft();
2065            }
2066            else {
2067                for (ParticipantStatusListener listener : participantStatusListeners) {
2068                    listener.kicked(from, mucUser.getItem().getActor(), mucUser.getItem().getReason());
2069                }
2070            }
2071        }
2072        // A user was banned from the room
2073        if (statusCodes.contains(Status.BANNED_301)) {
2074            // Check if this occupant was banned
2075            if (isUserModification) {
2076                joined = false;
2077                for (UserStatusListener listener : userStatusListeners) {
2078                    listener.banned(mucUser.getItem().getActor(), mucUser.getItem().getReason());
2079                }
2080
2081                // Reset occupant information.
2082                occupantsMap.clear();
2083                nickname = null;
2084                userHasLeft();
2085            }
2086            else {
2087                for (ParticipantStatusListener listener : participantStatusListeners) {
2088                    listener.banned(from, mucUser.getItem().getActor(), mucUser.getItem().getReason());
2089                }
2090            }
2091        }
2092        // A user's membership was revoked from the room
2093        if (statusCodes.contains(Status.REMOVED_AFFIL_CHANGE_321)) {
2094            // Check if this occupant's membership was revoked
2095            if (isUserModification) {
2096                joined = false;
2097                for (UserStatusListener listener : userStatusListeners) {
2098                    listener.membershipRevoked();
2099                }
2100
2101                // Reset occupant information.
2102                occupantsMap.clear();
2103                nickname = null;
2104                userHasLeft();
2105            }
2106        }
2107        // A occupant has changed his nickname in the room
2108        if (statusCodes.contains(Status.NEW_NICKNAME_303)) {
2109            for (ParticipantStatusListener listener : participantStatusListeners) {
2110                listener.nicknameChanged(from, mucUser.getItem().getNick());
2111            }
2112        }
2113    }
2114
2115    @Override
2116    public String toString() {
2117        return "MUC: " + room + "(" + connection.getUser() + ")";
2118    }
2119}