001/**
002 *
003 * Copyright 2003-2007 Jive Software. 2020-2021 Florian Schmaus
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.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.concurrent.ConcurrentHashMap;
026import java.util.concurrent.CopyOnWriteArrayList;
027import java.util.concurrent.CopyOnWriteArraySet;
028import java.util.logging.Level;
029import java.util.logging.Logger;
030
031import org.jivesoftware.smack.MessageListener;
032import org.jivesoftware.smack.PresenceListener;
033import org.jivesoftware.smack.SmackException;
034import org.jivesoftware.smack.SmackException.NoResponseException;
035import org.jivesoftware.smack.SmackException.NotConnectedException;
036import org.jivesoftware.smack.StanzaCollector;
037import org.jivesoftware.smack.StanzaListener;
038import org.jivesoftware.smack.XMPPConnection;
039import org.jivesoftware.smack.XMPPException;
040import org.jivesoftware.smack.XMPPException.XMPPErrorException;
041import org.jivesoftware.smack.chat.ChatMessageListener;
042import org.jivesoftware.smack.filter.AndFilter;
043import org.jivesoftware.smack.filter.FromMatchesFilter;
044import org.jivesoftware.smack.filter.MessageTypeFilter;
045import org.jivesoftware.smack.filter.MessageWithBodiesFilter;
046import org.jivesoftware.smack.filter.MessageWithSubjectFilter;
047import org.jivesoftware.smack.filter.MessageWithThreadFilter;
048import org.jivesoftware.smack.filter.NotFilter;
049import org.jivesoftware.smack.filter.OrFilter;
050import org.jivesoftware.smack.filter.PossibleFromTypeFilter;
051import org.jivesoftware.smack.filter.PresenceTypeFilter;
052import org.jivesoftware.smack.filter.StanzaExtensionFilter;
053import org.jivesoftware.smack.filter.StanzaFilter;
054import org.jivesoftware.smack.filter.StanzaIdFilter;
055import org.jivesoftware.smack.filter.StanzaTypeFilter;
056import org.jivesoftware.smack.filter.ToMatchesFilter;
057import org.jivesoftware.smack.packet.IQ;
058import org.jivesoftware.smack.packet.Message;
059import org.jivesoftware.smack.packet.MessageBuilder;
060import org.jivesoftware.smack.packet.MessageView;
061import org.jivesoftware.smack.packet.Presence;
062import org.jivesoftware.smack.packet.Stanza;
063import org.jivesoftware.smack.util.Objects;
064
065import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
066import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
067import org.jivesoftware.smackx.iqregister.packet.Registration;
068import org.jivesoftware.smackx.muc.MultiUserChatException.MissingMucCreationAcknowledgeException;
069import org.jivesoftware.smackx.muc.MultiUserChatException.MucAlreadyJoinedException;
070import org.jivesoftware.smackx.muc.MultiUserChatException.MucNotJoinedException;
071import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException;
072import org.jivesoftware.smackx.muc.filter.MUCUserStatusCodeFilter;
073import org.jivesoftware.smackx.muc.packet.Destroy;
074import org.jivesoftware.smackx.muc.packet.MUCAdmin;
075import org.jivesoftware.smackx.muc.packet.MUCInitialPresence;
076import org.jivesoftware.smackx.muc.packet.MUCItem;
077import org.jivesoftware.smackx.muc.packet.MUCOwner;
078import org.jivesoftware.smackx.muc.packet.MUCUser;
079import org.jivesoftware.smackx.muc.packet.MUCUser.Status;
080import org.jivesoftware.smackx.xdata.FormField;
081import org.jivesoftware.smackx.xdata.TextSingleFormField;
082import org.jivesoftware.smackx.xdata.form.FillableForm;
083import org.jivesoftware.smackx.xdata.form.Form;
084import org.jivesoftware.smackx.xdata.packet.DataForm;
085
086import org.jxmpp.jid.DomainBareJid;
087import org.jxmpp.jid.EntityBareJid;
088import org.jxmpp.jid.EntityFullJid;
089import org.jxmpp.jid.EntityJid;
090import org.jxmpp.jid.Jid;
091import org.jxmpp.jid.impl.JidCreate;
092import org.jxmpp.jid.parts.Resourcepart;
093
094/**
095 * A MultiUserChat room (XEP-45), created with {@link MultiUserChatManager#getMultiUserChat(EntityBareJid)}.
096 * <p>
097 * A MultiUserChat is a conversation that takes place among many users in a virtual
098 * room. A room could have many occupants with different affiliation and roles.
099 * Possible affiliations are "owner", "admin", "member", and "outcast". Possible roles
100 * are "moderator", "participant", and "visitor". Each role and affiliation guarantees
101 * different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
102 * Grant voice, Edit member list, etc.).
103 * </p>
104 * <p>
105 * <b>Note:</b> Make sure to leave the MUC ({@link #leave()}) when you don't need it anymore or
106 * otherwise you may leak the instance.
107 * </p>
108 *
109 * @author Gaston Dombiak
110 * @author Larry Kirschner
111 * @author Florian Schmaus
112 */
113public class MultiUserChat {
114    private static final Logger LOGGER = Logger.getLogger(MultiUserChat.class.getName());
115
116    private final XMPPConnection connection;
117    private final EntityBareJid room;
118    private final MultiUserChatManager multiUserChatManager;
119    private final Map<EntityFullJid, Presence> occupantsMap = new ConcurrentHashMap<>();
120
121    private final Set<InvitationRejectionListener> invitationRejectionListeners = new CopyOnWriteArraySet<InvitationRejectionListener>();
122    private final Set<SubjectUpdatedListener> subjectUpdatedListeners = new CopyOnWriteArraySet<SubjectUpdatedListener>();
123    private final Set<UserStatusListener> userStatusListeners = new CopyOnWriteArraySet<UserStatusListener>();
124    private final Set<ParticipantStatusListener> participantStatusListeners = new CopyOnWriteArraySet<ParticipantStatusListener>();
125    private final Set<MessageListener> messageListeners = new CopyOnWriteArraySet<MessageListener>();
126    private final Set<PresenceListener> presenceListeners = new CopyOnWriteArraySet<PresenceListener>();
127    private final Set<PresenceListener> presenceInterceptors = new CopyOnWriteArraySet<PresenceListener>();
128
129    /**
130     * This filter will match all stanzas send from the groupchat or from one if
131     * the groupchat participants, i.e. it filters only the bare JID of the from
132     * attribute against the JID of the MUC.
133     */
134    private final StanzaFilter fromRoomFilter;
135
136    /**
137     * Same as {@link #fromRoomFilter} together with {@link MessageTypeFilter#GROUPCHAT}.
138     */
139    private final StanzaFilter fromRoomGroupchatFilter;
140
141    private final StanzaListener presenceInterceptor;
142    private final StanzaListener messageListener;
143    private final StanzaListener presenceListener;
144    private final StanzaListener subjectListener;
145
146    private static final StanzaFilter DECLINE_FILTER = new AndFilter(MessageTypeFilter.NORMAL,
147                    new StanzaExtensionFilter(MUCUser.ELEMENT, MUCUser.NAMESPACE));
148    private final StanzaListener declinesListener;
149
150    private String subject;
151    private EntityFullJid myRoomJid;
152    private StanzaCollector messageCollector;
153
154    private DiscoverInfo mucServiceDiscoInfo;
155
156    /**
157     * Used to signal that the reflected self-presence was received <b>and</b> processed by us.
158     */
159    private volatile boolean processedReflectedSelfPresence;
160
161    private CopyOnWriteArrayList<MucMessageInterceptor> messageInterceptors;
162
163    MultiUserChat(XMPPConnection connection, EntityBareJid room, MultiUserChatManager multiUserChatManager) {
164        this.connection = connection;
165        this.room = room;
166        this.multiUserChatManager = multiUserChatManager;
167        this.messageInterceptors = MultiUserChatManager.getMessageInterceptors();
168
169        fromRoomFilter = FromMatchesFilter.create(room);
170        fromRoomGroupchatFilter = new AndFilter(fromRoomFilter, MessageTypeFilter.GROUPCHAT);
171
172        messageListener = new StanzaListener() {
173            @Override
174            public void processStanza(Stanza packet) throws NotConnectedException {
175                final Message message = (Message) packet;
176
177                for (MessageListener listener : messageListeners) {
178                            listener.processMessage(message);
179                }
180            }
181        };
182
183        // Create a listener for subject updates.
184        subjectListener = new StanzaListener() {
185            @Override
186            public void processStanza(Stanza packet) {
187                final Message msg = (Message) packet;
188                final EntityFullJid from = msg.getFrom().asEntityFullJidIfPossible();
189                // Update the room subject
190                subject = msg.getSubject();
191
192                // Fire event for subject updated listeners
193                for (SubjectUpdatedListener listener : subjectUpdatedListeners) {
194                    listener.subjectUpdated(msg.getSubject(), from);
195                }
196            }
197        };
198
199        // Create a listener for all presence updates.
200        presenceListener = new StanzaListener() {
201            @Override
202            public void processStanza(final Stanza packet) {
203                final Presence presence = (Presence) packet;
204                final EntityFullJid from = presence.getFrom().asEntityFullJidIfPossible();
205                if (from == null) {
206                    return;
207                }
208                final EntityFullJid myRoomJID = myRoomJid;
209                final boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
210                final MUCUser mucUser = MUCUser.from(packet);
211
212                switch (presence.getType()) {
213                case available:
214                    Presence oldPresence = occupantsMap.put(from, presence);
215                    if (mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110)) {
216                        processedReflectedSelfPresence = true;
217                        synchronized (this) {
218                            notify();
219                        }
220                    } else if (oldPresence != null) {
221                        // Get the previous occupant's affiliation & role
222                        MUCUser mucExtension = MUCUser.from(oldPresence);
223                        MUCAffiliation oldAffiliation = mucExtension.getItem().getAffiliation();
224                        MUCRole oldRole = mucExtension.getItem().getRole();
225                        // Get the new occupant's affiliation & role
226                        MUCAffiliation newAffiliation = mucUser.getItem().getAffiliation();
227                        MUCRole newRole = mucUser.getItem().getRole();
228                        // Fire role modification events
229                        checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
230                        // Fire affiliation modification events
231                        checkAffiliationModifications(
232                            oldAffiliation,
233                            newAffiliation,
234                            isUserStatusModification,
235                            from);
236                    } else {
237                        // A new occupant has joined the room
238                        for (ParticipantStatusListener listener : participantStatusListeners) {
239                            listener.joined(from);
240                        }
241                    }
242                    break;
243                case unavailable:
244                    occupantsMap.remove(from);
245                    if (mucUser != null && mucUser.hasStatus()) {
246                        if (isUserStatusModification) {
247                            userHasLeft();
248                        }
249                        // Fire events according to the received presence code
250                        checkPresenceCode(
251                            mucUser.getStatus(),
252                            isUserStatusModification,
253                            mucUser,
254                            from);
255                    } else {
256                        // An occupant has left the room
257                        if (!isUserStatusModification) {
258                            for (ParticipantStatusListener listener : participantStatusListeners) {
259                                listener.left(from);
260                            }
261                        }
262                    }
263
264                    Destroy destroy = mucUser.getDestroy();
265                    // The room has been destroyed.
266                    if (destroy != null) {
267                        EntityBareJid alternateMucJid = destroy.getJid();
268                        final MultiUserChat alternateMuc;
269                        if (alternateMucJid == null) {
270                            alternateMuc = null;
271                        } else {
272                            alternateMuc = multiUserChatManager.getMultiUserChat(alternateMucJid);
273                        }
274
275                        for (UserStatusListener listener : userStatusListeners) {
276                            listener.roomDestroyed(alternateMuc, destroy.getReason());
277                        }
278                    }
279
280                    if (isUserStatusModification) {
281                        for (UserStatusListener listener : userStatusListeners) {
282                            listener.removed(mucUser, presence);
283                        }
284                    } else {
285                        for (ParticipantStatusListener listener : participantStatusListeners) {
286                            listener.parted(from);
287                        }
288                    }
289                    break;
290                default:
291                    break;
292                }
293                for (PresenceListener listener : presenceListeners) {
294                    listener.processPresence(presence);
295                }
296            }
297        };
298
299        // Listens for all messages that include a MUCUser extension and fire the invitation
300        // rejection listeners if the message includes an invitation rejection.
301        declinesListener = new StanzaListener() {
302            @Override
303            public void processStanza(Stanza packet) {
304                Message message = (Message) packet;
305                // Get the MUC User extension
306                MUCUser mucUser = MUCUser.from(packet);
307                MUCUser.Decline rejection = mucUser.getDecline();
308                // Check if the MUCUser informs that the invitee has declined the invitation
309                if (rejection == null) {
310                    return;
311                }
312                // Fire event for invitation rejection listeners
313                fireInvitationRejectionListeners(message, rejection);
314            }
315        };
316
317        presenceInterceptor = new StanzaListener() {
318            @Override
319            public void processStanza(Stanza packet) {
320                Presence presence = (Presence) packet;
321                for (PresenceListener interceptor : presenceInterceptors) {
322                    interceptor.processPresence(presence);
323                }
324            }
325        };
326    }
327
328
329    /**
330     * Returns the name of the room this MultiUserChat object represents.
331     *
332     * @return the multi user chat room name.
333     */
334    public EntityBareJid getRoom() {
335        return room;
336    }
337
338    /**
339     * Enter a room, as described in XEP-45 7.2.
340     *
341     * @param conf the configuration used to enter the room.
342     * @return the returned presence by the service after the client send the initial presence in order to enter the room.
343     * @throws NotConnectedException if the XMPP connection is not connected.
344     * @throws NoResponseException if there was no response from the remote entity.
345     * @throws XMPPErrorException if there was an XMPP error returned.
346     * @throws InterruptedException if the calling thread was interrupted.
347     * @throws NotAMucServiceException if the entity is not a MUC serivce.
348     * @see <a href="http://xmpp.org/extensions/xep-0045.html#enter">XEP-45 7.2 Entering a Room</a>
349     */
350    private Presence enter(MucEnterConfiguration conf) throws NotConnectedException, NoResponseException,
351                    XMPPErrorException, InterruptedException, NotAMucServiceException {
352        final DomainBareJid mucService = room.asDomainBareJid();
353        mucServiceDiscoInfo = multiUserChatManager.getMucServiceDiscoInfo(mucService);
354        if (mucServiceDiscoInfo == null) {
355            throw new NotAMucServiceException(this);
356        }
357        // We enter a room by sending a presence packet where the "to"
358        // field is in the form "roomName@service/nickname"
359        Presence joinPresence = conf.getJoinPresence(this);
360
361        // Setup the messageListeners and presenceListeners *before* the join presence is send.
362        connection.addStanzaListener(messageListener, fromRoomGroupchatFilter);
363        StanzaFilter presenceFromRoomFilter = new AndFilter(fromRoomFilter,
364                        StanzaTypeFilter.PRESENCE,
365                        PossibleFromTypeFilter.ENTITY_FULL_JID);
366        connection.addStanzaListener(presenceListener, presenceFromRoomFilter);
367        // @formatter:off
368        connection.addStanzaListener(subjectListener,
369                        new AndFilter(fromRoomFilter,
370                                      MessageWithSubjectFilter.INSTANCE,
371                                      new NotFilter(MessageTypeFilter.ERROR),
372                                      // According to XEP-0045 § 8.1 "A message with a <subject/> and a <body/> or a <subject/> and a <thread/> is a
373                                      // legitimate message, but it SHALL NOT be interpreted as a subject change."
374                                      new NotFilter(MessageWithBodiesFilter.INSTANCE),
375                                      new NotFilter(MessageWithThreadFilter.INSTANCE))
376                        );
377        // @formatter:on
378        connection.addStanzaListener(declinesListener, new AndFilter(fromRoomFilter, DECLINE_FILTER));
379        connection.addStanzaSendingListener(presenceInterceptor, new AndFilter(ToMatchesFilter.create(room),
380                        StanzaTypeFilter.PRESENCE));
381        messageCollector = connection.createStanzaCollector(fromRoomGroupchatFilter);
382
383        // Wait for a presence packet back from the server.
384        // @formatter:off
385        StanzaFilter responseFilter = new AndFilter(StanzaTypeFilter.PRESENCE,
386                        new OrFilter(
387                            // We use a bare JID filter for positive responses, since the MUC service/room may rewrite the nickname.
388                            new AndFilter(FromMatchesFilter.createBare(getRoom()), MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF),
389                            // In case there is an error reply, we match on an error presence with the same stanza id and from the full
390                            // JID we send the join presence to.
391                            new AndFilter(FromMatchesFilter.createFull(joinPresence.getTo()), new StanzaIdFilter(joinPresence), PresenceTypeFilter.ERROR)
392                        )
393                    );
394        // @formatter:on
395        processedReflectedSelfPresence = false;
396        StanzaCollector presenceStanzaCollector = null;
397        final Presence reflectedSelfPresence;
398        try {
399            // This stanza collector will collect the final self presence from the MUC, which also signals that we have successful entered the MUC.
400            StanzaCollector selfPresenceCollector = connection.createStanzaCollectorAndSend(responseFilter, joinPresence);
401            StanzaCollector.Configuration presenceStanzaCollectorConfguration = StanzaCollector.newConfiguration().setCollectorToReset(
402                            selfPresenceCollector).setStanzaFilter(presenceFromRoomFilter);
403            // This stanza collector is used to reset the timeout of the selfPresenceCollector.
404            presenceStanzaCollector = connection.createStanzaCollector(presenceStanzaCollectorConfguration);
405            reflectedSelfPresence = selfPresenceCollector.nextResultOrThrow(conf.getTimeout());
406        }
407        catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) {
408            // Ensure that all callbacks are removed if there is an exception
409            removeConnectionCallbacks();
410            throw e;
411        }
412        finally {
413            if (presenceStanzaCollector != null) {
414                presenceStanzaCollector.cancel();
415            }
416        }
417
418        synchronized (presenceListener) {
419            // Only continue after we have received *and* processed the reflected self-presence. Since presences are
420            // handled in an extra listener, we may return from enter() without having processed all presences of the
421            // participants, resulting in a e.g. to low participant counter after enter(). Hence we wait here until the
422            // processing is done.
423            while (!processedReflectedSelfPresence) {
424                presenceListener.wait();
425            }
426        }
427
428        // This presence must be send from a full JID. We use the resourcepart of this JID as nick, since the room may
429        // performed roomnick rewriting
430        Resourcepart receivedNickname = reflectedSelfPresence.getFrom().getResourceOrThrow();
431        setNickname(receivedNickname);
432
433        // Update the list of joined rooms
434        multiUserChatManager.addJoinedRoom(room);
435        return reflectedSelfPresence;
436    }
437
438    private void setNickname(Resourcepart nickname) {
439        this.myRoomJid = JidCreate.entityFullFrom(room, nickname);
440    }
441
442    /**
443     * Get a new MUC enter configuration builder.
444     *
445     * @param nickname the nickname used when entering the MUC room.
446     * @return a new MUC enter configuration builder.
447     * @since 4.2
448     */
449    public MucEnterConfiguration.Builder getEnterConfigurationBuilder(Resourcepart nickname) {
450        return new MucEnterConfiguration.Builder(nickname, connection);
451    }
452
453    /**
454     * Creates the room according to some default configuration, assign the requesting user as the
455     * room owner, and add the owner to the room but not allow anyone else to enter the room
456     * (effectively "locking" the room). The requesting user will join the room under the specified
457     * nickname as soon as the room has been created.
458     * <p>
459     * To create an "Instant Room", that means a room with some default configuration that is
460     * available for immediate access, the room's owner should send an empty form after creating the
461     * room. Simply call {@link MucCreateConfigFormHandle#makeInstant()} on the returned {@link MucCreateConfigFormHandle}.
462     * </p>
463     * <p>
464     * To create a "Reserved Room", that means a room manually configured by the room creator before
465     * anyone is allowed to enter, the room's owner should complete and send a form after creating
466     * the room. Once the completed configuration form is sent to the server, the server will unlock
467     * the room. You can use the returned {@link MucCreateConfigFormHandle} to configure the room.
468     * </p>
469     *
470     * @param nickname the nickname to use.
471     * @return a handle to the MUC create configuration form API.
472     * @throws XMPPErrorException if the room couldn't be created for some reason (e.g. 405 error if
473     *         the user is not allowed to create the room)
474     * @throws NoResponseException if there was no response from the server.
475     * @throws InterruptedException if the calling thread was interrupted.
476     * @throws NotConnectedException if the XMPP connection is not connected.
477     * @throws MucAlreadyJoinedException if already joined the Multi-User Chat.7y
478     * @throws MissingMucCreationAcknowledgeException if there MUC creation was not acknowledged by the service.
479     * @throws NotAMucServiceException if the entity is not a MUC serivce.
480     */
481    public synchronized MucCreateConfigFormHandle create(Resourcepart nickname) throws NoResponseException,
482                    XMPPErrorException, InterruptedException, MucAlreadyJoinedException,
483                    NotConnectedException, MissingMucCreationAcknowledgeException, NotAMucServiceException {
484        if (isJoined()) {
485            throw new MucAlreadyJoinedException();
486        }
487
488        MucCreateConfigFormHandle mucCreateConfigFormHandle = createOrJoin(nickname);
489        if (mucCreateConfigFormHandle != null) {
490            // We successfully created a new room
491            return mucCreateConfigFormHandle;
492        }
493        // We need to leave the room since it seems that the room already existed
494        try {
495            leave();
496        }
497        catch (MucNotJoinedException e) {
498            LOGGER.log(Level.INFO, "Unexpected MucNotJoinedException", e);
499        }
500        throw new MissingMucCreationAcknowledgeException();
501    }
502
503    /**
504     * Create or join the MUC room with the given nickname.
505     *
506     * @param nickname the nickname to use in the MUC room.
507     * @return A {@link MucCreateConfigFormHandle} if the room was created while joining, or {@code null} if the room was just joined.
508     * @throws NoResponseException if there was no response from the remote entity.
509     * @throws XMPPErrorException if there was an XMPP error returned.
510     * @throws InterruptedException if the calling thread was interrupted.
511     * @throws NotConnectedException if the XMPP connection is not connected.
512     * @throws MucAlreadyJoinedException if already joined the Multi-User Chat.7y
513     * @throws NotAMucServiceException if the entity is not a MUC serivce.
514     */
515    public synchronized MucCreateConfigFormHandle createOrJoin(Resourcepart nickname) throws NoResponseException, XMPPErrorException,
516                    InterruptedException, MucAlreadyJoinedException, NotConnectedException, NotAMucServiceException {
517        MucEnterConfiguration mucEnterConfiguration = getEnterConfigurationBuilder(nickname).build();
518        return createOrJoin(mucEnterConfiguration);
519    }
520
521    /**
522     * Like {@link #create(Resourcepart)}, but will return a {@link MucCreateConfigFormHandle} if the room creation was acknowledged by
523     * the service (with an 201 status code). It's up to the caller to decide, based on the return
524     * value, if he needs to continue sending the room configuration. If {@code null} is returned, the room
525     * already existed and the user is able to join right away, without sending a form.
526     *
527     * @param mucEnterConfiguration the configuration used to enter the MUC.
528     * @return A {@link MucCreateConfigFormHandle} if the room was created while joining, or {@code null} if the room was just joined.
529     * @throws XMPPErrorException if the room couldn't be created for some reason (e.g. 405 error if
530     *         the user is not allowed to create the room)
531     * @throws NoResponseException if there was no response from the server.
532     * @throws InterruptedException if the calling thread was interrupted.
533     * @throws MucAlreadyJoinedException if the MUC is already joined
534     * @throws NotConnectedException if the XMPP connection is not connected.
535     * @throws NotAMucServiceException if the entity is not a MUC serivce.
536     */
537    public synchronized MucCreateConfigFormHandle createOrJoin(MucEnterConfiguration mucEnterConfiguration)
538                    throws NoResponseException, XMPPErrorException, InterruptedException, MucAlreadyJoinedException, NotConnectedException, NotAMucServiceException {
539        if (isJoined()) {
540            throw new MucAlreadyJoinedException();
541        }
542
543        Presence presence = enter(mucEnterConfiguration);
544
545        // Look for confirmation of room creation from the server
546        MUCUser mucUser = MUCUser.from(presence);
547        if (mucUser != null && mucUser.getStatus().contains(Status.ROOM_CREATED_201)) {
548            // Room was created and the user has joined the room
549            return new MucCreateConfigFormHandle();
550        }
551        return null;
552    }
553
554    /**
555     * A handle used to configure a newly created room. As long as the room is not configured it will be locked, which
556     * means that no one is able to join. The room will become unlocked as soon it got configured. In order to create an
557     * instant room, use {@link #makeInstant()}.
558     * <p>
559     * For advanced configuration options, use {@link MultiUserChat#getConfigurationForm()}, get the answer form with
560     * {@link Form#getFillableForm()}, fill it out and send it back to the room with
561     * {@link MultiUserChat#sendConfigurationForm(FillableForm)}.
562     * </p>
563     */
564    public class MucCreateConfigFormHandle {
565
566        /**
567         * Create an instant room. The default configuration will be accepted and the room will become unlocked, i.e.
568         * other users are able to join.
569         *
570         * @throws NoResponseException if there was no response from the remote entity.
571         * @throws XMPPErrorException if there was an XMPP error returned.
572         * @throws NotConnectedException if the XMPP connection is not connected.
573         * @throws InterruptedException if the calling thread was interrupted.
574         * @see <a href="http://www.xmpp.org/extensions/xep-0045.html#createroom-instant">XEP-45 § 10.1.2 Creating an
575         *      Instant Room</a>
576         */
577        public void makeInstant() throws NoResponseException, XMPPErrorException, NotConnectedException,
578                        InterruptedException {
579            sendConfigurationForm(null);
580        }
581
582        /**
583         * Alias for {@link MultiUserChat#getConfigFormManager()}.
584         *
585         * @return a MUC configuration form manager for this room.
586         * @throws NoResponseException if there was no response from the remote entity.
587         * @throws XMPPErrorException if there was an XMPP error returned.
588         * @throws NotConnectedException if the XMPP connection is not connected.
589         * @throws InterruptedException if the calling thread was interrupted.
590         * @see MultiUserChat#getConfigFormManager()
591         */
592        public MucConfigFormManager getConfigFormManager() throws NoResponseException,
593                        XMPPErrorException, NotConnectedException, InterruptedException {
594            return MultiUserChat.this.getConfigFormManager();
595        }
596    }
597
598    /**
599     * Create or join a MUC if it is necessary, i.e. if not the MUC is not already joined.
600     *
601     * @param nickname the required nickname to use.
602     * @param password the optional password required to join
603     * @return A {@link MucCreateConfigFormHandle} if the room was created while joining, or {@code null} if the room was just joined.
604     * @throws NoResponseException if there was no response from the remote entity.
605     * @throws XMPPErrorException if there was an XMPP error returned.
606     * @throws NotConnectedException if the XMPP connection is not connected.
607     * @throws InterruptedException if the calling thread was interrupted.
608     * @throws NotAMucServiceException if the entity is not a MUC serivce.
609     */
610    public MucCreateConfigFormHandle createOrJoinIfNecessary(Resourcepart nickname, String password) throws NoResponseException,
611                    XMPPErrorException, NotConnectedException, InterruptedException, NotAMucServiceException {
612        if (isJoined()) {
613            return null;
614        }
615        MucEnterConfiguration mucEnterConfiguration = getEnterConfigurationBuilder(nickname).withPassword(
616                        password).build();
617        try {
618            return createOrJoin(mucEnterConfiguration);
619        }
620        catch (MucAlreadyJoinedException e) {
621            return null;
622        }
623    }
624
625    /**
626     * Joins the chat room using the specified nickname. If already joined
627     * using another nickname, this method will first leave the room and then
628     * re-join using the new nickname. The default connection timeout for a reply
629     * from the group chat server that the join succeeded will be used. After
630     * joining the room, the room will decide the amount of history to send.
631     *
632     * @param nickname the nickname to use.
633     * @return the leave self-presence as reflected by the MUC.
634     * @throws NoResponseException if there was no response from the remote entity.
635     * @throws XMPPErrorException if an error occurs joining the room. In particular, a
636     *      401 error can occur if no password was provided and one is required; or a
637     *      403 error can occur if the user is banned; or a
638     *      404 error can occur if the room does not exist or is locked; or a
639     *      407 error can occur if user is not on the member list; or a
640     *      409 error can occur if someone is already in the group chat with the same nickname.
641     * @throws NoResponseException if there was no response from the server.
642     * @throws NotConnectedException if the XMPP connection is not connected.
643     * @throws InterruptedException if the calling thread was interrupted.
644     * @throws NotAMucServiceException if the entity is not a MUC serivce.
645     */
646    public Presence join(Resourcepart nickname) throws NoResponseException, XMPPErrorException,
647                    NotConnectedException, InterruptedException, NotAMucServiceException {
648        MucEnterConfiguration.Builder builder = getEnterConfigurationBuilder(nickname);
649        Presence reflectedJoinPresence = join(builder.build());
650        return reflectedJoinPresence;
651    }
652
653    /**
654     * Joins the chat room using the specified nickname and password. If already joined
655     * using another nickname, this method will first leave the room and then
656     * re-join using the new nickname. The default connection timeout for a reply
657     * from the group chat server that the join succeeded will be used. After
658     * joining the room, the room will decide the amount of history to send.<p>
659     *
660     * A password is required when joining password protected rooms. If the room does
661     * not require a password there is no need to provide one.
662     *
663     * @param nickname the nickname to use.
664     * @param password the password to use.
665     * @throws XMPPErrorException if an error occurs joining the room. In particular, a
666     *      401 error can occur if no password was provided and one is required; or a
667     *      403 error can occur if the user is banned; or a
668     *      404 error can occur if the room does not exist or is locked; or a
669     *      407 error can occur if user is not on the member list; or a
670     *      409 error can occur if someone is already in the group chat with the same nickname.
671     * @throws InterruptedException if the calling thread was interrupted.
672     * @throws NotConnectedException if the XMPP connection is not connected.
673     * @throws NoResponseException if there was no response from the server.
674     * @throws NotAMucServiceException if the entity is not a MUC serivce.
675     */
676    public void join(Resourcepart nickname, String password) throws XMPPErrorException, InterruptedException, NoResponseException, NotConnectedException, NotAMucServiceException {
677        MucEnterConfiguration.Builder builder = getEnterConfigurationBuilder(nickname).withPassword(
678                        password);
679        join(builder.build());
680    }
681
682    /**
683     * Joins the chat room using the specified nickname and password. If already joined
684     * using another nickname, this method will first leave the room and then
685     * re-join using the new nickname.<p>
686     *
687     * To control the amount of history to receive while joining a room you will need to provide
688     * a configured DiscussionHistory object.<p>
689     *
690     * A password is required when joining password protected rooms. If the room does
691     * not require a password there is no need to provide one.<p>
692     *
693     * If the room does not already exist when the user seeks to enter it, the server will
694     * decide to create a new room or not.
695     *
696     * @param mucEnterConfiguration the configuration used to enter the MUC.
697     * @return the join self-presence as reflected by the MUC.
698     * @throws XMPPErrorException if an error occurs joining the room. In particular, a
699     *      401 error can occur if no password was provided and one is required; or a
700     *      403 error can occur if the user is banned; or a
701     *      404 error can occur if the room does not exist or is locked; or a
702     *      407 error can occur if user is not on the member list; or a
703     *      409 error can occur if someone is already in the group chat with the same nickname.
704     * @throws NoResponseException if there was no response from the server.
705     * @throws NotConnectedException if the XMPP connection is not connected.
706     * @throws InterruptedException if the calling thread was interrupted.
707     * @throws NotAMucServiceException if the entity is not a MUC serivce.
708     */
709    public synchronized Presence join(MucEnterConfiguration mucEnterConfiguration)
710        throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException, NotAMucServiceException {
711        // If we've already joined the room, leave it before joining under a new
712        // nickname.
713        if (isJoined()) {
714            try {
715                leaveSync();
716            }
717            catch (XMPPErrorException | NoResponseException | MucNotJoinedException e) {
718                LOGGER.log(Level.WARNING, "Could not leave MUC prior joining, assuming we are not joined", e);
719            }
720        }
721        Presence reflectedJoinPresence = enter(mucEnterConfiguration);
722        return reflectedJoinPresence;
723    }
724
725    /**
726     * Returns true if currently in the multi user chat (after calling the {@link
727     * #join(Resourcepart)} method).
728     *
729     * @return true if currently in the multi user chat room.
730     */
731    public boolean isJoined() {
732        return myRoomJid != null;
733    }
734
735    /**
736     * Leave the chat room.
737     *
738     * @return the leave presence as reflected by the MUC.
739     * @throws NotConnectedException if the XMPP connection is not connected.
740     * @throws InterruptedException if the calling thread was interrupted.
741     * @throws XMPPErrorException if there was an XMPP error returned.
742     * @throws NoResponseException if there was no response from the remote entity.
743     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
744     * @deprecated use {@link #leave()} instead.
745     */
746    @Deprecated
747    // TODO: Remove in Smack 4.5.
748    public synchronized Presence leaveSync() throws NotConnectedException, InterruptedException, MucNotJoinedException, NoResponseException, XMPPErrorException {
749        return leave();
750    }
751
752    /**
753     * Leave the chat room.
754     *
755     * @return the leave presence as reflected by the MUC.
756     * @throws NotConnectedException if the XMPP connection is not connected.
757     * @throws InterruptedException if the calling thread was interrupted.
758     * @throws XMPPErrorException if there was an XMPP error returned.
759     * @throws NoResponseException if there was no response from the remote entity.
760     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
761     */
762    public synchronized Presence leave()
763                    throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, MucNotJoinedException {
764        //  Note that this method is intentionally not guarded by
765        // "if  (!joined) return" because it should be always be possible to leave the room in case the instance's
766        // state does not reflect the actual state.
767
768        final EntityFullJid myRoomJid = this.myRoomJid;
769        if (myRoomJid == null) {
770            throw new MucNotJoinedException(this);
771        }
772
773        // TODO: Consider adding a origin-id to the presence, once it is moved form smack-experimental into
774        // smack-extensions, in case the MUC service does not support stable IDs, and modify
775        // reflectedLeavePresenceFilters accordingly.
776
777        // We leave a room by sending a presence packet where the "to"
778        // field is in the form "roomName@service/nickname"
779        Presence leavePresence = connection.getStanzaFactory().buildPresenceStanza()
780                .ofType(Presence.Type.unavailable)
781                .to(myRoomJid)
782                .build();
783
784        List<StanzaFilter> reflectedLeavePresenceFilters = new ArrayList<>(3);
785        reflectedLeavePresenceFilters.add(StanzaTypeFilter.PRESENCE);
786        reflectedLeavePresenceFilters.add(new OrFilter(
787                        new AndFilter(FromMatchesFilter.createFull(myRoomJid), PresenceTypeFilter.UNAVAILABLE,
788                                        MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF),
789                        new AndFilter(fromRoomFilter, PresenceTypeFilter.ERROR)));
790
791        if (serviceSupportsStableIds()) {
792            reflectedLeavePresenceFilters.add(new StanzaIdFilter(leavePresence));
793        }
794
795        StanzaFilter reflectedLeavePresenceFilter = new AndFilter(reflectedLeavePresenceFilters);
796
797        Presence reflectedLeavePresence;
798        try {
799            reflectedLeavePresence = connection.createStanzaCollectorAndSend(reflectedLeavePresenceFilter, leavePresence).nextResultOrThrow();
800        } finally {
801            // Reset occupant information after we send the leave presence. This ensures that we only call userHasLeft()
802            // and reset the local MUC state after we successfully left the MUC (or if an exception occurred).
803            userHasLeft();
804        }
805
806        return reflectedLeavePresence;
807    }
808
809    /**
810     * Get a {@link MucConfigFormManager} to configure this room.
811     * <p>
812     * Only room owners are able to configure a room.
813     * </p>
814     *
815     * @return a MUC configuration form manager for this room.
816     * @throws NoResponseException if there was no response from the remote entity.
817     * @throws XMPPErrorException if there was an XMPP error returned.
818     * @throws NotConnectedException if the XMPP connection is not connected.
819     * @throws InterruptedException if the calling thread was interrupted.
820     * @see <a href="http://xmpp.org/extensions/xep-0045.html#roomconfig">XEP-45 § 10.2 Subsequent Room Configuration</a>
821     * @since 4.2
822     */
823    public MucConfigFormManager getConfigFormManager() throws NoResponseException,
824                    XMPPErrorException, NotConnectedException, InterruptedException {
825        return new MucConfigFormManager(this);
826    }
827
828    /**
829     * Returns the room's configuration form that the room's owner can use.
830     * The configuration form allows to set the room's language,
831     * enable logging, specify room's type, etc..
832     *
833     * @return the Form that contains the fields to complete together with the instrucions or
834     * <code>null</code> if no configuration is possible.
835     * @throws XMPPErrorException if an error occurs asking the configuration form for the room.
836     * @throws NoResponseException if there was no response from the server.
837     * @throws NotConnectedException if the XMPP connection is not connected.
838     * @throws InterruptedException if the calling thread was interrupted.
839     */
840    public Form getConfigurationForm() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
841        MUCOwner iq = new MUCOwner();
842        iq.setTo(room);
843        iq.setType(IQ.Type.get);
844
845        IQ answer = connection.sendIqRequestAndWaitForResponse(iq);
846        DataForm dataForm = DataForm.from(answer, MucConfigFormManager.FORM_TYPE);
847        return new Form(dataForm);
848    }
849
850    /**
851     * Sends the completed configuration form to the server. The room will be configured
852     * with the new settings defined in the form.
853     *
854     * @param form the form with the new settings.
855     * @throws XMPPErrorException if an error occurs setting the new rooms' configuration.
856     * @throws NoResponseException if there was no response from the server.
857     * @throws NotConnectedException if the XMPP connection is not connected.
858     * @throws InterruptedException if the calling thread was interrupted.
859     */
860    public void sendConfigurationForm(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
861        final DataForm dataForm;
862        if (form != null) {
863            dataForm = form.getDataFormToSubmit();
864        } else {
865            // Instant room, cf. XEP-0045 § 10.1.2
866            dataForm = DataForm.builder().build();
867        }
868
869        MUCOwner iq = new MUCOwner();
870        iq.setTo(room);
871        iq.setType(IQ.Type.set);
872        iq.addExtension(dataForm);
873
874        connection.sendIqRequestAndWaitForResponse(iq);
875    }
876
877    /**
878     * Returns the room's registration form that an unaffiliated user, can use to become a member
879     * of the room or <code>null</code> if no registration is possible. Some rooms may restrict the
880     * privilege to register members and allow only room admins to add new members.<p>
881     *
882     * If the user requesting registration requirements is not allowed to register with the room
883     * (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
884     * error to the user (error code 405).
885     *
886     * @return the registration Form that contains the fields to complete together with the
887     * instrucions or <code>null</code> if no registration is possible.
888     * @throws XMPPErrorException if an error occurs asking the registration form for the room or a
889     * 405 error if the user is not allowed to register with the room.
890     * @throws NoResponseException if there was no response from the server.
891     * @throws NotConnectedException if the XMPP connection is not connected.
892     * @throws InterruptedException if the calling thread was interrupted.
893     */
894    public Form getRegistrationForm() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
895        Registration reg = new Registration();
896        reg.setType(IQ.Type.get);
897        reg.setTo(room);
898
899        IQ result = connection.sendIqRequestAndWaitForResponse(reg);
900        DataForm dataForm = DataForm.from(result);
901        return new Form(dataForm);
902    }
903
904    /**
905     * Sends the completed registration form to the server. After the user successfully submits
906     * the form, the room may queue the request for review by the room admins or may immediately
907     * add the user to the member list by changing the user's affiliation from "none" to "member.<p>
908     *
909     * If the desired room nickname is already reserved for that room, the room will return a
910     * "Conflict" error to the user (error code 409). If the room does not support registration,
911     * it will return a "Service Unavailable" error to the user (error code 503).
912     *
913     * @param form the completed registration form.
914     * @throws XMPPErrorException if an error occurs submitting the registration form. In particular, a
915     *      409 error can occur if the desired room nickname is already reserved for that room;
916     *      or a 503 error can occur if the room does not support registration.
917     * @throws NoResponseException if there was no response from the server.
918     * @throws NotConnectedException if the XMPP connection is not connected.
919     * @throws InterruptedException if the calling thread was interrupted.
920     */
921    public void sendRegistrationForm(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
922        Registration reg = new Registration();
923        reg.setType(IQ.Type.set);
924        reg.setTo(room);
925        reg.addExtension(form.getDataFormToSubmit());
926
927        connection.sendIqRequestAndWaitForResponse(reg);
928    }
929
930    /**
931     * Sends a request to destroy the room.
932     *
933     * @throws XMPPErrorException if an error occurs while trying to destroy the room.
934     *      An error can occur which will be wrapped by an XMPPException --
935     *      XMPP error code 403. The error code can be used to present more
936     *      appropriate error messages to end-users.
937     * @throws NoResponseException if there was no response from the server.
938     * @throws NotConnectedException if the XMPP connection is not connected.
939     * @throws InterruptedException if the calling thread was interrupted.
940     * @see #destroy(String, EntityBareJid)
941     * @since 4.5
942     */
943    public void destroy() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
944        destroy(null, null);
945    }
946
947    /**
948     * Sends a request to the server to destroy the room. The sender of the request
949     * should be the room's owner. If the sender of the destroy request is not the room's owner
950     * then the server will answer a "Forbidden" error (403).
951     *
952     * @param reason an optional reason for the room destruction.
953     * @param alternateJID an optional JID of an alternate location.
954     * @throws XMPPErrorException if an error occurs while trying to destroy the room.
955     *      An error can occur which will be wrapped by an XMPPException --
956     *      XMPP error code 403. The error code can be used to present more
957     *      appropriate error messages to end-users.
958     * @throws NoResponseException if there was no response from the server.
959     * @throws NotConnectedException if the XMPP connection is not connected.
960     * @throws InterruptedException if the calling thread was interrupted.
961     */
962    public void destroy(String reason, EntityBareJid alternateJID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
963        MUCOwner iq = new MUCOwner();
964        iq.setTo(room);
965        iq.setType(IQ.Type.set);
966
967        // Create the reason for the room destruction
968        Destroy destroy = new Destroy(alternateJID, reason);
969        iq.setDestroy(destroy);
970
971        try {
972            connection.sendIqRequestAndWaitForResponse(iq);
973        }
974        catch (XMPPErrorException e) {
975            // Note that we do not call userHasLeft() here because an XMPPErrorException would usually indicate that the
976            // room was not destroyed and we therefore we also did not leave the room.
977            throw e;
978        }
979        catch (NoResponseException | NotConnectedException | InterruptedException e) {
980            // Reset occupant information.
981            userHasLeft();
982            throw e;
983        }
984
985        // Reset occupant information.
986        userHasLeft();
987    }
988
989    /**
990     * Invites another user to the room in which one is an occupant. The invitation
991     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
992     *
993     * If the room is password-protected, the invitee will receive a password to use to join
994     * the room. If the room is members-only, the the invitee may be added to the member list.
995     *
996     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
997     * @param reason the reason why the user is being invited.
998     * @throws NotConnectedException if the XMPP connection is not connected.
999     * @throws InterruptedException if the calling thread was interrupted.
1000     */
1001    public void invite(EntityBareJid user, String reason) throws NotConnectedException, InterruptedException {
1002        invite(connection.getStanzaFactory().buildMessageStanza(), user, reason);
1003    }
1004
1005    /**
1006     * Invites another user to the room in which one is an occupant using a given Message. The invitation
1007     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
1008     *
1009     * If the room is password-protected, the invitee will receive a password to use to join
1010     * the room. If the room is members-only, the the invitee may be added to the member list.
1011     *
1012     * @param message the message to use for sending the invitation.
1013     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
1014     * @param reason the reason why the user is being invited.
1015     * @throws NotConnectedException if the XMPP connection is not connected.
1016     * @throws InterruptedException if the calling thread was interrupted.
1017     * @deprecated use {@link #invite(MessageBuilder, EntityBareJid, String)} instead.
1018     */
1019    @Deprecated
1020    // TODO: Remove in Smack 4.5.
1021    public void invite(Message message, EntityBareJid user, String reason) throws NotConnectedException, InterruptedException {
1022        // TODO listen for 404 error code when inviter supplies a non-existent JID
1023        message.setTo(room);
1024
1025        // Create the MUCUser packet that will include the invitation
1026        MUCUser mucUser = new MUCUser();
1027        MUCUser.Invite invite = new MUCUser.Invite(reason, user);
1028        mucUser.setInvite(invite);
1029        // Add the MUCUser packet that includes the invitation to the message
1030        message.addExtension(mucUser);
1031
1032        connection.sendStanza(message);
1033    }
1034
1035    /**
1036     * Invites another user to the room in which one is an occupant using a given Message. The invitation
1037     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
1038     *
1039     * If the room is password-protected, the invitee will receive a password to use to join
1040     * the room. If the room is members-only, the the invitee may be added to the member list.
1041     *
1042     * @param messageBuilder the message to use for sending the invitation.
1043     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
1044     * @param reason the reason why the user is being invited.
1045     * @throws NotConnectedException if the XMPP connection is not connected.
1046     * @throws InterruptedException if the calling thread was interrupted.
1047     */
1048    public void invite(MessageBuilder messageBuilder, EntityBareJid user, String reason) throws NotConnectedException, InterruptedException {
1049        // TODO listen for 404 error code when inviter supplies a non-existent JID
1050        messageBuilder.to(room);
1051
1052        // Create the MUCUser packet that will include the invitation
1053        MUCUser mucUser = new MUCUser();
1054        MUCUser.Invite invite = new MUCUser.Invite(reason, user);
1055        mucUser.setInvite(invite);
1056        // Add the MUCUser packet that includes the invitation to the message
1057        messageBuilder.addExtension(mucUser);
1058
1059        Message message = messageBuilder.build();
1060        connection.sendStanza(message);
1061    }
1062
1063    /**
1064     * Adds a listener to invitation rejections notifications. The listener will be fired anytime
1065     * an invitation is declined.
1066     *
1067     * @param listener an invitation rejection listener.
1068     * @return true if the listener was not already added.
1069     */
1070    public boolean addInvitationRejectionListener(InvitationRejectionListener listener) {
1071         return invitationRejectionListeners.add(listener);
1072    }
1073
1074    /**
1075     * Removes a listener from invitation rejections notifications. The listener will be fired
1076     * anytime an invitation is declined.
1077     *
1078     * @param listener an invitation rejection listener.
1079     * @return true if the listener was registered and is now removed.
1080     */
1081    public boolean removeInvitationRejectionListener(InvitationRejectionListener listener) {
1082        return invitationRejectionListeners.remove(listener);
1083    }
1084
1085    /**
1086     * Fires invitation rejection listeners.
1087     *
1088     * @param message the message.
1089     * @param rejection the information about the rejection.
1090     */
1091    private void fireInvitationRejectionListeners(Message message, MUCUser.Decline rejection) {
1092        EntityBareJid invitee = rejection.getFrom();
1093        String reason = rejection.getReason();
1094        InvitationRejectionListener[] listeners;
1095        synchronized (invitationRejectionListeners) {
1096            listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
1097            invitationRejectionListeners.toArray(listeners);
1098        }
1099        for (InvitationRejectionListener listener : listeners) {
1100            listener.invitationDeclined(invitee, reason, message, rejection);
1101        }
1102    }
1103
1104    /**
1105     * Adds a listener to subject change notifications. The listener will be fired anytime
1106     * the room's subject changes.
1107     *
1108     * @param listener a subject updated listener.
1109     * @return true if the listener was not already added.
1110     */
1111    public boolean addSubjectUpdatedListener(SubjectUpdatedListener listener) {
1112        return subjectUpdatedListeners.add(listener);
1113    }
1114
1115    /**
1116     * Removes a listener from subject change notifications. The listener will be fired
1117     * anytime the room's subject changes.
1118     *
1119     * @param listener a subject updated listener.
1120     * @return true if the listener was registered and is now removed.
1121     */
1122    public boolean removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
1123        return subjectUpdatedListeners.remove(listener);
1124    }
1125
1126    /**
1127     * Adds a new {@link StanzaListener} that will be invoked every time a new presence
1128     * is going to be sent by this MultiUserChat to the server. Stanza interceptors may
1129     * add new extensions to the presence that is going to be sent to the MUC service.
1130     *
1131     * @param presenceInterceptor the new stanza interceptor that will intercept presence packets.
1132     */
1133    public void addPresenceInterceptor(PresenceListener presenceInterceptor) {
1134        presenceInterceptors.add(presenceInterceptor);
1135    }
1136
1137    /**
1138     * Removes a {@link StanzaListener} that was being invoked every time a new presence
1139     * was being sent by this MultiUserChat to the server. Stanza interceptors may
1140     * add new extensions to the presence that is going to be sent to the MUC service.
1141     *
1142     * @param presenceInterceptor the stanza interceptor to remove.
1143     */
1144    public void removePresenceInterceptor(PresenceListener presenceInterceptor) {
1145        presenceInterceptors.remove(presenceInterceptor);
1146    }
1147
1148    /**
1149     * Returns the last known room's subject or <code>null</code> if the user hasn't joined the room
1150     * or the room does not have a subject yet. In case the room has a subject, as soon as the
1151     * user joins the room a message with the current room's subject will be received.<p>
1152     *
1153     * To be notified every time the room's subject change you should add a listener
1154     * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
1155     *
1156     * To change the room's subject use {@link #changeSubject(String)}.
1157     *
1158     * @return the room's subject or <code>null</code> if the user hasn't joined the room or the
1159     * room does not have a subject yet.
1160     */
1161    public String getSubject() {
1162        return subject;
1163    }
1164
1165    /**
1166     * Returns the reserved room nickname for the user in the room. A user may have a reserved
1167     * nickname, for example through explicit room registration or database integration. In such
1168     * cases it may be desirable for the user to discover the reserved nickname before attempting
1169     * to enter the room.
1170     *
1171     * @return the reserved room nickname or <code>null</code> if none.
1172     * @throws SmackException if there was no response from the server.
1173     * @throws InterruptedException if the calling thread was interrupted.
1174     */
1175    public String getReservedNickname() throws SmackException, InterruptedException {
1176        try {
1177            DiscoverInfo result =
1178                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
1179                    room,
1180                    "x-roomuser-item");
1181            // Look for an Identity that holds the reserved nickname and return its name
1182            for (DiscoverInfo.Identity identity : result.getIdentities()) {
1183                return identity.getName();
1184            }
1185        }
1186        catch (XMPPException e) {
1187            LOGGER.log(Level.SEVERE, "Error retrieving room nickname", e);
1188        }
1189        // If no Identity was found then the user does not have a reserved room nickname
1190        return null;
1191    }
1192
1193    /**
1194     * Returns the nickname that was used to join the room, or <code>null</code> if not
1195     * currently joined.
1196     *
1197     * @return the nickname currently being used.
1198     */
1199    public Resourcepart getNickname() {
1200        final EntityFullJid myRoomJid = this.myRoomJid;
1201        if (myRoomJid == null) {
1202            return null;
1203        }
1204        return myRoomJid.getResourcepart();
1205    }
1206
1207    /**
1208     * Changes the occupant's nickname to a new nickname within the room. Each room occupant
1209     * will receive two presence packets. One of type "unavailable" for the old nickname and one
1210     * indicating availability for the new nickname. The unavailable presence will contain the new
1211     * nickname and an appropriate status code (namely 303) as extended presence information. The
1212     * status code 303 indicates that the occupant is changing his/her nickname.
1213     *
1214     * @param nickname the new nickname within the room.
1215     * @throws XMPPErrorException if the new nickname is already in use by another occupant.
1216     * @throws NoResponseException if there was no response from the server.
1217     * @throws NotConnectedException if the XMPP connection is not connected.
1218     * @throws InterruptedException if the calling thread was interrupted.
1219     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
1220     */
1221    public synchronized void changeNickname(Resourcepart nickname) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, MucNotJoinedException  {
1222        Objects.requireNonNull(nickname, "Nickname must not be null or blank.");
1223        // Check that we already have joined the room before attempting to change the
1224        // nickname.
1225        if (!isJoined()) {
1226            throw new MucNotJoinedException(this);
1227        }
1228        final EntityFullJid jid = JidCreate.entityFullFrom(room, nickname);
1229        // We change the nickname by sending a presence packet where the "to"
1230        // field is in the form "roomName@service/nickname"
1231        // We don't have to signal the MUC support again
1232        Presence joinPresence = connection.getStanzaFactory().buildPresenceStanza()
1233                .to(jid)
1234                .ofType(Presence.Type.available)
1235                .build();
1236
1237        // Wait for a presence packet back from the server.
1238        StanzaFilter responseFilter =
1239            new AndFilter(
1240                FromMatchesFilter.createFull(jid),
1241                new StanzaTypeFilter(Presence.class));
1242        StanzaCollector response = connection.createStanzaCollectorAndSend(responseFilter, joinPresence);
1243        // Wait up to a certain number of seconds for a reply. If there is a negative reply, an
1244        // exception will be thrown
1245        response.nextResultOrThrow();
1246
1247        // TODO: Shouldn't this handle nickname rewriting by the MUC service?
1248        setNickname(nickname);
1249    }
1250
1251    /**
1252     * Changes the occupant's availability status within the room. The presence type
1253     * will remain available but with a new status that describes the presence update and
1254     * a new presence mode (e.g. Extended away).
1255     *
1256     * @param status a text message describing the presence update.
1257     * @param mode the mode type for the presence update.
1258     * @throws NotConnectedException if the XMPP connection is not connected.
1259     * @throws InterruptedException if the calling thread was interrupted.
1260     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
1261     */
1262    public void changeAvailabilityStatus(String status, Presence.Mode mode) throws NotConnectedException, InterruptedException, MucNotJoinedException {
1263        final EntityFullJid myRoomJid = this.myRoomJid;
1264        if (myRoomJid == null) {
1265            throw new MucNotJoinedException(this);
1266        }
1267
1268        // We change the availability status by sending a presence packet to the room with the
1269        // new presence status and mode
1270        Presence joinPresence = connection.getStanzaFactory().buildPresenceStanza()
1271                .to(myRoomJid)
1272                .ofType(Presence.Type.available)
1273                .setStatus(status)
1274                .setMode(mode)
1275                .build();
1276
1277        // Send join packet.
1278        connection.sendStanza(joinPresence);
1279    }
1280
1281    /**
1282     * Kicks a visitor or participant from the room. The kicked occupant will receive a presence
1283     * of type "unavailable" including a status code 307 and optionally along with the reason
1284     * (if provided) and the bare JID of the user who initiated the kick. After the occupant
1285     * was kicked from the room, the rest of the occupants will receive a presence of type
1286     * "unavailable". The presence will include a status code 307 which means that the occupant
1287     * was kicked from the room.
1288     *
1289     * @param nickname the nickname of the participant or visitor to kick from the room
1290     * (e.g. "john").
1291     * @param reason the reason why the participant or visitor is being kicked from the room.
1292     * @throws XMPPErrorException if an error occurs kicking the occupant. In particular, a
1293     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1294     *      was intended to be kicked (i.e. Not Allowed error); or a
1295     *      403 error can occur if the occupant that intended to kick another occupant does
1296     *      not have kicking privileges (i.e. Forbidden error); or a
1297     *      400 error can occur if the provided nickname is not present in the room.
1298     * @throws NoResponseException if there was no response from the server.
1299     * @throws NotConnectedException if the XMPP connection is not connected.
1300     * @throws InterruptedException if the calling thread was interrupted.
1301     */
1302    public void kickParticipant(Resourcepart nickname, String reason) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1303        changeRole(nickname, MUCRole.none, reason);
1304    }
1305
1306    /**
1307     * Sends a voice request to the MUC. The room moderators usually need to approve this request.
1308     *
1309     * @throws NotConnectedException if the XMPP connection is not connected.
1310     * @throws InterruptedException if the calling thread was interrupted.
1311     * @see <a href="http://xmpp.org/extensions/xep-0045.html#requestvoice">XEP-45 § 7.13 Requesting
1312     *      Voice</a>
1313     * @since 4.1
1314     */
1315    public void requestVoice() throws NotConnectedException, InterruptedException {
1316        DataForm.Builder form = DataForm.builder()
1317                        .setFormType(MUCInitialPresence.NAMESPACE + "#request");
1318
1319        TextSingleFormField.Builder requestVoiceField = FormField.textSingleBuilder("muc#role");
1320        requestVoiceField.setLabel("Requested role");
1321        requestVoiceField.setValue("participant");
1322        form.addField(requestVoiceField.build());
1323
1324        Message message = connection.getStanzaFactory().buildMessageStanza()
1325                .to(room)
1326                .addExtension(form.build())
1327                .build();
1328        connection.sendStanza(message);
1329    }
1330
1331    /**
1332     * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
1333     * who does and does not have "voice" in the room. To have voice means that a room occupant
1334     * is able to send messages to the room occupants.
1335     *
1336     * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
1337     * @throws XMPPErrorException if an error occurs granting voice to a visitor. In particular, a
1338     *      403 error can occur if the occupant that intended to grant voice is not
1339     *      a moderator in this room (i.e. Forbidden error); or a
1340     *      400 error can occur if the provided nickname is not present in the room.
1341     * @throws NoResponseException if there was no response from the server.
1342     * @throws NotConnectedException if the XMPP connection is not connected.
1343     * @throws InterruptedException if the calling thread was interrupted.
1344     */
1345    public void grantVoice(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1346        changeRole(nicknames, MUCRole.participant);
1347    }
1348
1349    /**
1350     * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
1351     * who does and does not have "voice" in the room. To have voice means that a room occupant
1352     * is able to send messages to the room occupants.
1353     *
1354     * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
1355     * @throws XMPPErrorException if an error occurs granting voice to a visitor. In particular, a
1356     *      403 error can occur if the occupant that intended to grant voice is not
1357     *      a moderator in this room (i.e. Forbidden error); or a
1358     *      400 error can occur if the provided nickname is not present in the room.
1359     * @throws NoResponseException if there was no response from the server.
1360     * @throws NotConnectedException if the XMPP connection is not connected.
1361     * @throws InterruptedException if the calling thread was interrupted.
1362     */
1363    public void grantVoice(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1364        changeRole(nickname, MUCRole.participant, null);
1365    }
1366
1367    /**
1368     * Revokes voice from participants in the room. In a moderated room, a moderator may want to
1369     * revoke an occupant's privileges to speak. To have voice means that a room occupant
1370     * is able to send messages to the room occupants.
1371     *
1372     * @param nicknames the nicknames of the participants to revoke voice (e.g. "john").
1373     * @throws XMPPErrorException if an error occurs revoking voice from a participant. In particular, a
1374     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1375     *      was tried to revoke his voice (i.e. Not Allowed error); or a
1376     *      400 error can occur if the provided nickname is not present in the room.
1377     * @throws NoResponseException if there was no response from the server.
1378     * @throws NotConnectedException if the XMPP connection is not connected.
1379     * @throws InterruptedException if the calling thread was interrupted.
1380     */
1381    public void revokeVoice(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1382        changeRole(nicknames, MUCRole.visitor);
1383    }
1384
1385    /**
1386     * Revokes voice from a participant in the room. In a moderated room, a moderator may want to
1387     * revoke an occupant's privileges to speak. To have voice means that a room occupant
1388     * is able to send messages to the room occupants.
1389     *
1390     * @param nickname the nickname of the participant to revoke voice (e.g. "john").
1391     * @throws XMPPErrorException if an error occurs revoking voice from a participant. In particular, a
1392     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1393     *      was tried to revoke his voice (i.e. Not Allowed error); or a
1394     *      400 error can occur if the provided nickname is not present in the room.
1395     * @throws NoResponseException if there was no response from the server.
1396     * @throws NotConnectedException if the XMPP connection is not connected.
1397     * @throws InterruptedException if the calling thread was interrupted.
1398     */
1399    public void revokeVoice(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1400        changeRole(nickname, MUCRole.visitor, null);
1401    }
1402
1403    /**
1404     * Bans users from the room. An admin or owner of the room can ban users from a room. This
1405     * means that the banned user will no longer be able to join the room unless the ban has been
1406     * removed. If the banned user was present in the room then he/she will be removed from the
1407     * room and notified that he/she was banned along with the reason (if provided) and the bare
1408     * XMPP user ID of the user who initiated the ban.
1409     *
1410     * @param jids the bare XMPP user IDs of the users to ban.
1411     * @throws XMPPErrorException if an error occurs banning a user. In particular, a
1412     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1413     *      was tried to be banned (i.e. Not Allowed error).
1414     * @throws NoResponseException if there was no response from the server.
1415     * @throws NotConnectedException if the XMPP connection is not connected.
1416     * @throws InterruptedException if the calling thread was interrupted.
1417     */
1418    public void banUsers(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1419        changeAffiliationByAdmin(jids, MUCAffiliation.outcast);
1420    }
1421
1422    /**
1423     * Bans a user from the room. An admin or owner of the room can ban users from a room. This
1424     * means that the banned user will no longer be able to join the room unless the ban has been
1425     * removed. If the banned user was present in the room then he/she will be removed from the
1426     * room and notified that he/she was banned along with the reason (if provided) and the bare
1427     * XMPP user ID of the user who initiated the ban.
1428     *
1429     * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org").
1430     * @param reason the optional reason why the user was banned.
1431     * @throws XMPPErrorException if an error occurs banning a user. In particular, a
1432     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
1433     *      was tried to be banned (i.e. Not Allowed error).
1434     * @throws NoResponseException if there was no response from the server.
1435     * @throws NotConnectedException if the XMPP connection is not connected.
1436     * @throws InterruptedException if the calling thread was interrupted.
1437     */
1438    public void banUser(Jid jid, String reason) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1439        changeAffiliationByAdmin(jid, MUCAffiliation.outcast, reason);
1440    }
1441
1442    /**
1443     * Grants membership to other users. Only administrators are able to grant membership. A user
1444     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1445     * that a user cannot enter without being on the member list).
1446     *
1447     * @param jids the XMPP user IDs of the users to grant membership.
1448     * @throws XMPPErrorException if an error occurs granting membership to a user.
1449     * @throws NoResponseException if there was no response from the server.
1450     * @throws NotConnectedException if the XMPP connection is not connected.
1451     * @throws InterruptedException if the calling thread was interrupted.
1452     */
1453    public void grantMembership(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1454        changeAffiliationByAdmin(jids, MUCAffiliation.member);
1455    }
1456
1457    /**
1458     * Grants membership to a user. Only administrators are able to grant membership. A user
1459     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1460     * that a user cannot enter without being on the member list).
1461     *
1462     * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org").
1463     * @throws XMPPErrorException if an error occurs granting membership to a user.
1464     * @throws NoResponseException if there was no response from the server.
1465     * @throws NotConnectedException if the XMPP connection is not connected.
1466     * @throws InterruptedException if the calling thread was interrupted.
1467     */
1468    public void grantMembership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1469        changeAffiliationByAdmin(jid, MUCAffiliation.member, null);
1470    }
1471
1472    /**
1473     * Revokes users' membership. Only administrators are able to revoke membership. A user
1474     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1475     * that a user cannot enter without being on the member list). If the user is in the room and
1476     * the room is of type members-only then the user will be removed from the room.
1477     *
1478     * @param jids the bare XMPP user IDs of the users to revoke membership.
1479     * @throws XMPPErrorException if an error occurs revoking membership to a user.
1480     * @throws NoResponseException if there was no response from the server.
1481     * @throws NotConnectedException if the XMPP connection is not connected.
1482     * @throws InterruptedException if the calling thread was interrupted.
1483     */
1484    public void revokeMembership(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1485        changeAffiliationByAdmin(jids, MUCAffiliation.none);
1486    }
1487
1488    /**
1489     * Revokes a user's membership. Only administrators are able to revoke membership. A user
1490     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
1491     * that a user cannot enter without being on the member list). If the user is in the room and
1492     * the room is of type members-only then the user will be removed from the room.
1493     *
1494     * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org").
1495     * @throws XMPPErrorException if an error occurs revoking membership to a user.
1496     * @throws NoResponseException if there was no response from the server.
1497     * @throws NotConnectedException if the XMPP connection is not connected.
1498     * @throws InterruptedException if the calling thread was interrupted.
1499     */
1500    public void revokeMembership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1501        changeAffiliationByAdmin(jid, MUCAffiliation.none, null);
1502    }
1503
1504    /**
1505     * Grants moderator privileges to participants or visitors. Room administrators may grant
1506     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
1507     * other users, modify room's subject plus all the partcipants privileges.
1508     *
1509     * @param nicknames the nicknames of the occupants to grant moderator privileges.
1510     * @throws XMPPErrorException if an error occurs granting moderator privileges to a user.
1511     * @throws NoResponseException if there was no response from the server.
1512     * @throws NotConnectedException if the XMPP connection is not connected.
1513     * @throws InterruptedException if the calling thread was interrupted.
1514     */
1515    public void grantModerator(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1516        changeRole(nicknames, MUCRole.moderator);
1517    }
1518
1519    /**
1520     * Grants moderator privileges to a participant or visitor. Room administrators may grant
1521     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
1522     * other users, modify room's subject plus all the partcipants privileges.
1523     *
1524     * @param nickname the nickname of the occupant to grant moderator privileges.
1525     * @throws XMPPErrorException if an error occurs granting moderator privileges to a user.
1526     * @throws NoResponseException if there was no response from the server.
1527     * @throws NotConnectedException if the XMPP connection is not connected.
1528     * @throws InterruptedException if the calling thread was interrupted.
1529     */
1530    public void grantModerator(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1531        changeRole(nickname, MUCRole.moderator, null);
1532    }
1533
1534    /**
1535     * Revokes moderator privileges from other users. The occupant that loses moderator
1536     * privileges will become a participant. Room administrators may revoke moderator privileges
1537     * only to occupants whose affiliation is member or none. This means that an administrator is
1538     * not allowed to revoke moderator privileges from other room administrators or owners.
1539     *
1540     * @param nicknames the nicknames of the occupants to revoke moderator privileges.
1541     * @throws XMPPErrorException if an error occurs revoking moderator privileges from a user.
1542     * @throws NoResponseException if there was no response from the server.
1543     * @throws NotConnectedException if the XMPP connection is not connected.
1544     * @throws InterruptedException if the calling thread was interrupted.
1545     */
1546    public void revokeModerator(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1547        changeRole(nicknames, MUCRole.participant);
1548    }
1549
1550    /**
1551     * Revokes moderator privileges from another user. The occupant that loses moderator
1552     * privileges will become a participant. Room administrators may revoke moderator privileges
1553     * only to occupants whose affiliation is member or none. This means that an administrator is
1554     * not allowed to revoke moderator privileges from other room administrators or owners.
1555     *
1556     * @param nickname the nickname of the occupant to revoke moderator privileges.
1557     * @throws XMPPErrorException if an error occurs revoking moderator privileges from a user.
1558     * @throws NoResponseException if there was no response from the server.
1559     * @throws NotConnectedException if the XMPP connection is not connected.
1560     * @throws InterruptedException if the calling thread was interrupted.
1561     */
1562    public void revokeModerator(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1563        changeRole(nickname, MUCRole.participant, null);
1564    }
1565
1566    /**
1567     * Grants ownership privileges to other users. Room owners may grant ownership privileges.
1568     * Some room implementations will not allow to grant ownership privileges to other users.
1569     * An owner is allowed to change defining room features as well as perform all administrative
1570     * functions.
1571     *
1572     * @param jids the collection of bare XMPP user IDs of the users to grant ownership.
1573     * @throws XMPPErrorException if an error occurs granting ownership privileges to a user.
1574     * @throws NoResponseException if there was no response from the server.
1575     * @throws NotConnectedException if the XMPP connection is not connected.
1576     * @throws InterruptedException if the calling thread was interrupted.
1577     */
1578    public void grantOwnership(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1579        changeAffiliationByAdmin(jids, MUCAffiliation.owner);
1580    }
1581
1582    /**
1583     * Grants ownership privileges to another user. Room owners may grant ownership privileges.
1584     * Some room implementations will not allow to grant ownership privileges to other users.
1585     * An owner is allowed to change defining room features as well as perform all administrative
1586     * functions.
1587     *
1588     * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org").
1589     * @throws XMPPErrorException if an error occurs granting ownership privileges to a user.
1590     * @throws NoResponseException if there was no response from the server.
1591     * @throws NotConnectedException if the XMPP connection is not connected.
1592     * @throws InterruptedException if the calling thread was interrupted.
1593     */
1594    public void grantOwnership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1595        changeAffiliationByAdmin(jid, MUCAffiliation.owner, null);
1596    }
1597
1598    /**
1599     * Revokes ownership privileges from other users. The occupant that loses ownership
1600     * privileges will become an administrator. Room owners may revoke ownership privileges.
1601     * Some room implementations will not allow to grant ownership privileges to other users.
1602     *
1603     * @param jids the bare XMPP user IDs of the users to revoke ownership.
1604     * @throws XMPPErrorException if an error occurs revoking ownership privileges from a user.
1605     * @throws NoResponseException if there was no response from the server.
1606     * @throws NotConnectedException if the XMPP connection is not connected.
1607     * @throws InterruptedException if the calling thread was interrupted.
1608     */
1609    public void revokeOwnership(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1610        changeAffiliationByAdmin(jids, MUCAffiliation.admin);
1611    }
1612
1613    /**
1614     * Revokes ownership privileges from another user. The occupant that loses ownership
1615     * privileges will become an administrator. Room owners may revoke ownership privileges.
1616     * Some room implementations will not allow to grant ownership privileges to other users.
1617     *
1618     * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org").
1619     * @throws XMPPErrorException if an error occurs revoking ownership privileges from a user.
1620     * @throws NoResponseException if there was no response from the server.
1621     * @throws NotConnectedException if the XMPP connection is not connected.
1622     * @throws InterruptedException if the calling thread was interrupted.
1623     */
1624    public void revokeOwnership(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1625        changeAffiliationByAdmin(jid, MUCAffiliation.admin, null);
1626    }
1627
1628    /**
1629     * Grants administrator privileges to other users. Room owners may grant administrator
1630     * privileges to a member or unaffiliated user. An administrator is allowed to perform
1631     * administrative functions such as banning users and edit moderator list.
1632     *
1633     * @param jids the bare XMPP user IDs of the users to grant administrator privileges.
1634     * @throws XMPPErrorException if an error occurs granting administrator privileges to a user.
1635     * @throws NoResponseException if there was no response from the server.
1636     * @throws NotConnectedException if the XMPP connection is not connected.
1637     * @throws InterruptedException if the calling thread was interrupted.
1638     */
1639    public void grantAdmin(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1640        changeAffiliationByAdmin(jids, MUCAffiliation.admin);
1641    }
1642
1643    /**
1644     * Grants administrator privileges to another user. Room owners may grant administrator
1645     * privileges to a member or unaffiliated user. An administrator is allowed to perform
1646     * administrative functions such as banning users and edit moderator list.
1647     *
1648     * @param jid the bare XMPP user ID of the user to grant administrator privileges
1649     * (e.g. "user@host.org").
1650     * @throws XMPPErrorException if an error occurs granting administrator privileges to a user.
1651     * @throws NoResponseException if there was no response from the server.
1652     * @throws NotConnectedException if the XMPP connection is not connected.
1653     * @throws InterruptedException if the calling thread was interrupted.
1654     */
1655    public void grantAdmin(Jid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1656        changeAffiliationByAdmin(jid, MUCAffiliation.admin);
1657    }
1658
1659    /**
1660     * Revokes administrator privileges from users. The occupant that loses administrator
1661     * privileges will become a member. Room owners may revoke administrator privileges from
1662     * a member or unaffiliated user.
1663     *
1664     * @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
1665     * @throws XMPPErrorException if an error occurs revoking administrator privileges from a user.
1666     * @throws NoResponseException if there was no response from the server.
1667     * @throws NotConnectedException if the XMPP connection is not connected.
1668     * @throws InterruptedException if the calling thread was interrupted.
1669     */
1670    public void revokeAdmin(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1671        changeAffiliationByAdmin(jids, MUCAffiliation.admin);
1672    }
1673
1674    /**
1675     * Revokes administrator privileges from a user. The occupant that loses administrator
1676     * privileges will become a member. Room owners may revoke administrator privileges from
1677     * a member or unaffiliated user.
1678     *
1679     * @param jid the bare XMPP user ID of the user to revoke administrator privileges
1680     * (e.g. "user@host.org").
1681     * @throws XMPPErrorException if an error occurs revoking administrator privileges from a user.
1682     * @throws NoResponseException if there was no response from the server.
1683     * @throws NotConnectedException if the XMPP connection is not connected.
1684     * @throws InterruptedException if the calling thread was interrupted.
1685     */
1686    public void revokeAdmin(EntityJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
1687        changeAffiliationByAdmin(jid, MUCAffiliation.member);
1688    }
1689
1690    /**
1691     * Tries to change the affiliation with an 'muc#admin' namespace
1692     *
1693     * @param jid TODO javadoc me please
1694     * @param affiliation TODO javadoc me please
1695     * @throws XMPPErrorException if there was an XMPP error returned.
1696     * @throws NoResponseException if there was no response from the remote entity.
1697     * @throws NotConnectedException if the XMPP connection is not connected.
1698     * @throws InterruptedException if the calling thread was interrupted.
1699     */
1700    private void changeAffiliationByAdmin(Jid jid, MUCAffiliation affiliation)
1701                    throws NoResponseException, XMPPErrorException,
1702                    NotConnectedException, InterruptedException {
1703        changeAffiliationByAdmin(jid, affiliation, null);
1704    }
1705
1706    /**
1707     * Tries to change the affiliation with an 'muc#admin' namespace
1708     *
1709     * @param jid TODO javadoc me please
1710     * @param affiliation TODO javadoc me please
1711     * @param reason the reason for the affiliation change (optional)
1712     * @throws XMPPErrorException if there was an XMPP error returned.
1713     * @throws NoResponseException if there was no response from the remote entity.
1714     * @throws NotConnectedException if the XMPP connection is not connected.
1715     * @throws InterruptedException if the calling thread was interrupted.
1716     */
1717    private void changeAffiliationByAdmin(Jid jid, MUCAffiliation affiliation, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1718        MUCAdmin iq = new MUCAdmin();
1719        iq.setTo(room);
1720        iq.setType(IQ.Type.set);
1721        // Set the new affiliation.
1722        MUCItem item = new MUCItem(affiliation, jid, reason);
1723        iq.addItem(item);
1724
1725        connection.sendIqRequestAndWaitForResponse(iq);
1726    }
1727
1728    private void changeAffiliationByAdmin(Collection<? extends Jid> jids, MUCAffiliation affiliation)
1729                    throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1730        MUCAdmin iq = new MUCAdmin();
1731        iq.setTo(room);
1732        iq.setType(IQ.Type.set);
1733        for (Jid jid : jids) {
1734            // Set the new affiliation.
1735            MUCItem item = new MUCItem(affiliation, jid);
1736            iq.addItem(item);
1737        }
1738
1739        connection.sendIqRequestAndWaitForResponse(iq);
1740    }
1741
1742    private void changeRole(Resourcepart nickname, MUCRole role, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1743        MUCAdmin iq = new MUCAdmin();
1744        iq.setTo(room);
1745        iq.setType(IQ.Type.set);
1746        // Set the new role.
1747        MUCItem item = new MUCItem(role, nickname, reason);
1748        iq.addItem(item);
1749
1750        connection.sendIqRequestAndWaitForResponse(iq);
1751    }
1752
1753    private void changeRole(Collection<Resourcepart> nicknames, MUCRole role) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
1754        MUCAdmin iq = new MUCAdmin();
1755        iq.setTo(room);
1756        iq.setType(IQ.Type.set);
1757        for (Resourcepart nickname : nicknames) {
1758            // Set the new role.
1759            MUCItem item = new MUCItem(role, nickname);
1760            iq.addItem(item);
1761        }
1762
1763        connection.sendIqRequestAndWaitForResponse(iq);
1764    }
1765
1766    /**
1767     * Returns the number of occupants in the group chat.<p>
1768     *
1769     * Note: this value will only be accurate after joining the group chat, and
1770     * may fluctuate over time. If you query this value directly after joining the
1771     * group chat it may not be accurate, as it takes a certain amount of time for
1772     * the server to send all presence packets to this client.
1773     *
1774     * @return the number of occupants in the group chat.
1775     */
1776    public int getOccupantsCount() {
1777        return occupantsMap.size();
1778    }
1779
1780    /**
1781     * Returns an List  for the list of fully qualified occupants
1782     * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
1783     * Typically, a client would only display the nickname of the occupant. To
1784     * get the nickname from the fully qualified name, use the
1785     * {@link org.jxmpp.util.XmppStringUtils#parseResource(String)} method.
1786     * Note: this value will only be accurate after joining the group chat, and may
1787     * fluctuate over time.
1788     *
1789     * @return a List of the occupants in the group chat.
1790     */
1791    public List<EntityFullJid> getOccupants() {
1792        return new ArrayList<>(occupantsMap.keySet());
1793    }
1794
1795    /**
1796     * Returns the presence info for a particular user, or <code>null</code> if the user
1797     * is not in the room.<p>
1798     *
1799     * @param user the room occupant to search for his presence. The format of user must
1800     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
1801     * @return the occupant's current presence, or <code>null</code> if the user is unavailable
1802     *      or if no presence information is available.
1803     */
1804    public Presence getOccupantPresence(EntityFullJid user) {
1805        return occupantsMap.get(user);
1806    }
1807
1808    /**
1809     * Returns the Occupant information for a particular occupant, or <code>null</code> if the
1810     * user is not in the room. The Occupant object may include information such as full
1811     * JID of the user as well as the role and affiliation of the user in the room.<p>
1812     *
1813     * @param user the room occupant to search for his presence. The format of user must
1814     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
1815     * @return the Occupant or <code>null</code> if the user is unavailable (i.e. not in the room).
1816     */
1817    public Occupant getOccupant(EntityFullJid user) {
1818        Presence presence = getOccupantPresence(user);
1819        if (presence != null) {
1820            return new Occupant(presence);
1821        }
1822        return null;
1823    }
1824
1825    /**
1826     * Adds a stanza listener that will be notified of any new Presence packets
1827     * sent to the group chat. Using a listener is a suitable way to know when the list
1828     * of occupants should be re-loaded due to any changes.
1829     *
1830     * @param listener a stanza listener that will be notified of any presence packets
1831     *      sent to the group chat.
1832     * @return true if the listener was not already added.
1833     */
1834    public boolean addParticipantListener(PresenceListener listener) {
1835        return presenceListeners.add(listener);
1836    }
1837
1838    /**
1839     * Removes a stanza listener that was being notified of any new Presence packets
1840     * sent to the group chat.
1841     *
1842     * @param listener a stanza listener that was being notified of any presence packets
1843     *      sent to the group chat.
1844     * @return true if the listener was removed, otherwise the listener was not added previously.
1845     */
1846    public boolean removeParticipantListener(PresenceListener listener) {
1847        return presenceListeners.remove(listener);
1848    }
1849
1850    /**
1851     * Returns a list of <code>Affiliate</code> with the room owners.
1852     *
1853     * @return a list of <code>Affiliate</code> with the room owners.
1854     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1855     * @throws NoResponseException if there was no response from the server.
1856     * @throws NotConnectedException if the XMPP connection is not connected.
1857     * @throws InterruptedException if the calling thread was interrupted.
1858     */
1859    public List<Affiliate> getOwners() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1860        return getAffiliatesByAdmin(MUCAffiliation.owner);
1861    }
1862
1863    /**
1864     * Returns a list of <code>Affiliate</code> with the room administrators.
1865     *
1866     * @return a list of <code>Affiliate</code> with the room administrators.
1867     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1868     * @throws NoResponseException if there was no response from the server.
1869     * @throws NotConnectedException if the XMPP connection is not connected.
1870     * @throws InterruptedException if the calling thread was interrupted.
1871     */
1872    public List<Affiliate> getAdmins() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1873        return getAffiliatesByAdmin(MUCAffiliation.admin);
1874    }
1875
1876    /**
1877     * Returns a list of <code>Affiliate</code> with the room members.
1878     *
1879     * @return a list of <code>Affiliate</code> with the room members.
1880     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1881     * @throws NoResponseException if there was no response from the server.
1882     * @throws NotConnectedException if the XMPP connection is not connected.
1883     * @throws InterruptedException if the calling thread was interrupted.
1884     */
1885    public List<Affiliate> getMembers() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
1886        return getAffiliatesByAdmin(MUCAffiliation.member);
1887    }
1888
1889    /**
1890     * Returns a list of <code>Affiliate</code> with the room outcasts.
1891     *
1892     * @return a list of <code>Affiliate</code> with the room outcasts.
1893     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1894     * @throws NoResponseException if there was no response from the server.
1895     * @throws NotConnectedException if the XMPP connection is not connected.
1896     * @throws InterruptedException if the calling thread was interrupted.
1897     */
1898    public List<Affiliate> getOutcasts() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1899        return getAffiliatesByAdmin(MUCAffiliation.outcast);
1900    }
1901
1902    /**
1903     * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
1904     * sending a request in the admin namespace.
1905     *
1906     * @param affiliation the affiliation of the users in the room.
1907     * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
1908     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1909     * @throws NoResponseException if there was no response from the server.
1910     * @throws NotConnectedException if the XMPP connection is not connected.
1911     * @throws InterruptedException if the calling thread was interrupted.
1912     */
1913    private List<Affiliate> getAffiliatesByAdmin(MUCAffiliation affiliation) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1914        MUCAdmin iq = new MUCAdmin();
1915        iq.setTo(room);
1916        iq.setType(IQ.Type.get);
1917        // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
1918        MUCItem item = new MUCItem(affiliation);
1919        iq.addItem(item);
1920
1921        MUCAdmin answer = (MUCAdmin) connection.sendIqRequestAndWaitForResponse(iq);
1922
1923        // Get the list of affiliates from the server's answer
1924        List<Affiliate> affiliates = new ArrayList<Affiliate>();
1925        for (MUCItem mucadminItem : answer.getItems()) {
1926            affiliates.add(new Affiliate(mucadminItem));
1927        }
1928        return affiliates;
1929    }
1930
1931    /**
1932     * Returns a list of <code>Occupant</code> with the room moderators.
1933     *
1934     * @return a list of <code>Occupant</code> with the room moderators.
1935     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1936     * @throws NoResponseException if there was no response from the server.
1937     * @throws NotConnectedException if the XMPP connection is not connected.
1938     * @throws InterruptedException if the calling thread was interrupted.
1939     */
1940    public List<Occupant> getModerators() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1941        return getOccupants(MUCRole.moderator);
1942    }
1943
1944    /**
1945     * Returns a list of <code>Occupant</code> with the room participants.
1946     *
1947     * @return a list of <code>Occupant</code> with the room participants.
1948     * @throws XMPPErrorException if you don't have enough privileges to get this information.
1949     * @throws NoResponseException if there was no response from the server.
1950     * @throws NotConnectedException if the XMPP connection is not connected.
1951     * @throws InterruptedException if the calling thread was interrupted.
1952     */
1953    public List<Occupant> getParticipants() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1954        return getOccupants(MUCRole.participant);
1955    }
1956
1957    /**
1958     * Returns a list of <code>Occupant</code> that have the specified room role.
1959     *
1960     * @param role the role of the occupant in the room.
1961     * @return a list of <code>Occupant</code> that have the specified room role.
1962     * @throws XMPPErrorException if an error occurred while performing the request to the server or you
1963     *         don't have enough privileges to get this information.
1964     * @throws NoResponseException if there was no response from the server.
1965     * @throws NotConnectedException if the XMPP connection is not connected.
1966     * @throws InterruptedException if the calling thread was interrupted.
1967     */
1968    private List<Occupant> getOccupants(MUCRole role) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
1969        MUCAdmin iq = new MUCAdmin();
1970        iq.setTo(room);
1971        iq.setType(IQ.Type.get);
1972        // Set the specified role. This may request the list of moderators/participants.
1973        MUCItem item = new MUCItem(role);
1974        iq.addItem(item);
1975
1976        MUCAdmin answer = (MUCAdmin) connection.sendIqRequestAndWaitForResponse(iq);
1977        // Get the list of participants from the server's answer
1978        List<Occupant> participants = new ArrayList<Occupant>();
1979        for (MUCItem mucadminItem : answer.getItems()) {
1980            participants.add(new Occupant(mucadminItem));
1981        }
1982        return participants;
1983    }
1984
1985    /**
1986     * Sends a message to the chat room.
1987     *
1988     * @param text the text of the message to send.
1989     * @throws NotConnectedException if the XMPP connection is not connected.
1990     * @throws InterruptedException if the calling thread was interrupted.
1991     */
1992    public void sendMessage(String text) throws NotConnectedException, InterruptedException {
1993        Message message = buildMessage()
1994                .setBody(text)
1995                .build();
1996        connection.sendStanza(message);
1997    }
1998
1999    /**
2000     * Returns a new Chat for sending private messages to a given room occupant.
2001     * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
2002     * service will change the 'from' address to the sender's room JID and delivering the message
2003     * to the intended recipient's full JID.
2004     *
2005     * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul').
2006     * @param listener the listener is a message listener that will handle messages for the newly
2007     * created chat.
2008     * @return new Chat for sending private messages to a given room occupant.
2009     */
2010    // TODO This should be made new not using chat.Chat. Private MUC chats are different from XMPP-IM 1:1 chats in to many ways.
2011    // API sketch: PrivateMucChat createPrivateChat(Resourcepart nick)
2012    @SuppressWarnings("deprecation")
2013    public org.jivesoftware.smack.chat.Chat createPrivateChat(EntityFullJid occupant, ChatMessageListener listener) {
2014        return org.jivesoftware.smack.chat.ChatManager.getInstanceFor(connection).createChat(occupant, listener);
2015    }
2016
2017    /**
2018     * Creates a new Message to send to the chat room.
2019     *
2020     * @return a new Message addressed to the chat room.
2021     * @deprecated use {@link #buildMessage()} instead.
2022     */
2023    @Deprecated
2024    // TODO: Remove when stanza builder is ready.
2025    public Message createMessage() {
2026        return connection.getStanzaFactory().buildMessageStanza()
2027                .ofType(Message.Type.groupchat)
2028                .to(room)
2029                .build();
2030    }
2031
2032    /**
2033     * Constructs a new message builder for messages send to this MUC room.
2034     *
2035     * @return a new message builder.
2036     */
2037    public MessageBuilder buildMessage() {
2038        return connection.getStanzaFactory()
2039                .buildMessageStanza()
2040                .ofType(Message.Type.groupchat)
2041                .to(room)
2042                ;
2043    }
2044
2045    /**
2046     * Sends a Message to the chat room.
2047     *
2048     * @param message the message.
2049     * @throws NotConnectedException if the XMPP connection is not connected.
2050     * @throws InterruptedException if the calling thread was interrupted.
2051     * @deprecated use {@link #sendMessage(MessageBuilder)} instead.
2052     */
2053    @Deprecated
2054    // TODO: Remove in Smack 4.5.
2055    public void sendMessage(Message message) throws NotConnectedException, InterruptedException {
2056        sendMessage(message.asBuilder());
2057    }
2058
2059    /**
2060     * Sends a Message to the chat room.
2061     *
2062     * @param messageBuilder the message.
2063     * @return a read-only view of the send message.
2064     * @throws NotConnectedException if the XMPP connection is not connected.
2065     * @throws InterruptedException if the calling thread was interrupted.
2066     */
2067    public MessageView sendMessage(MessageBuilder messageBuilder) throws NotConnectedException, InterruptedException {
2068        for (MucMessageInterceptor interceptor : messageInterceptors) {
2069            interceptor.intercept(messageBuilder, this);
2070        }
2071
2072        Message message = messageBuilder.to(room).ofType(Message.Type.groupchat).build();
2073        connection.sendStanza(message);
2074        return message;
2075    }
2076
2077    /**
2078    * Polls for and returns the next message, or <code>null</code> if there isn't
2079    * a message immediately available. This method provides significantly different
2080    * functionalty than the {@link #nextMessage()} method since it's non-blocking.
2081    * In other words, the method call will always return immediately, whereas the
2082    * nextMessage method will return only when a message is available (or after
2083    * a specific timeout).
2084    *
2085    * @return the next message if one is immediately available and
2086    *      <code>null</code> otherwise.
2087     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
2088    */
2089    public Message pollMessage() throws MucNotJoinedException {
2090        if (messageCollector == null) {
2091            throw new MucNotJoinedException(this);
2092        }
2093        return messageCollector.pollResult();
2094    }
2095
2096    /**
2097     * Returns the next available message in the chat. The method call will block
2098     * (not return) until a message is available.
2099     *
2100     * @return the next message.
2101     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
2102     * @throws InterruptedException if the calling thread was interrupted.
2103     */
2104    public Message nextMessage() throws MucNotJoinedException, InterruptedException {
2105        if (messageCollector == null) {
2106            throw new MucNotJoinedException(this);
2107        }
2108        return  messageCollector.nextResultBlockForever();
2109    }
2110
2111    /**
2112     * Returns the next available message in the chat. The method call will block
2113     * (not return) until a stanza is available or the <code>timeout</code> has elapased.
2114     * If the timeout elapses without a result, <code>null</code> will be returned.
2115     *
2116     * @param timeout the maximum amount of time to wait for the next message.
2117     * @return the next message, or <code>null</code> if the timeout elapses without a
2118     *      message becoming available.
2119     * @throws MucNotJoinedException if not joined to the Multi-User Chat.
2120     * @throws InterruptedException if the calling thread was interrupted.
2121     */
2122    public Message nextMessage(long timeout) throws MucNotJoinedException, InterruptedException {
2123        if (messageCollector == null) {
2124            throw new MucNotJoinedException(this);
2125        }
2126        return messageCollector.nextResult(timeout);
2127    }
2128
2129    /**
2130     * Adds a stanza listener that will be notified of any new messages in the
2131     * group chat. Only "group chat" messages addressed to this group chat will
2132     * be delivered to the listener. If you wish to listen for other packets
2133     * that may be associated with this group chat, you should register a
2134     * PacketListener directly with the XMPPConnection with the appropriate
2135     * PacketListener.
2136     *
2137     * @param listener a stanza listener.
2138     * @return true if the listener was not already added.
2139     */
2140    public boolean addMessageListener(MessageListener listener) {
2141        return messageListeners.add(listener);
2142    }
2143
2144    /**
2145     * Removes a stanza listener that was being notified of any new messages in the
2146     * multi user chat. Only "group chat" messages addressed to this multi user chat were
2147     * being delivered to the listener.
2148     *
2149     * @param listener a stanza listener.
2150     * @return true if the listener was removed, otherwise the listener was not added previously.
2151     */
2152    public boolean removeMessageListener(MessageListener listener) {
2153        return messageListeners.remove(listener);
2154    }
2155
2156    public boolean addMessageInterceptor(MucMessageInterceptor interceptor) {
2157        return messageInterceptors.add(interceptor);
2158    }
2159
2160    public boolean removeMessageInterceptor(MucMessageInterceptor interceptor) {
2161        return messageInterceptors.remove(interceptor);
2162    }
2163
2164    /**
2165     * Changes the subject within the room. As a default, only users with a role of "moderator"
2166     * are allowed to change the subject in a room. Although some rooms may be configured to
2167     * allow a mere participant or even a visitor to change the subject.
2168     *
2169     * @param subject the new room's subject to set.
2170     * @throws XMPPErrorException if someone without appropriate privileges attempts to change the
2171     *          room subject will throw an error with code 403 (i.e. Forbidden)
2172     * @throws NoResponseException if there was no response from the server.
2173     * @throws NotConnectedException if the XMPP connection is not connected.
2174     * @throws InterruptedException if the calling thread was interrupted.
2175     */
2176    public void changeSubject(final String subject) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
2177        MessageBuilder message = buildMessage();
2178        message.setSubject(subject);
2179        // Wait for an error or confirmation message back from the server.
2180        StanzaFilter responseFilter = new AndFilter(fromRoomGroupchatFilter, new StanzaFilter() {
2181            @Override
2182            public boolean accept(Stanza packet) {
2183                Message msg = (Message) packet;
2184                return subject.equals(msg.getSubject());
2185            }
2186        });
2187        StanzaCollector response = connection.createStanzaCollectorAndSend(responseFilter, message.build());
2188        // Wait up to a certain number of seconds for a reply.
2189        response.nextResultOrThrow();
2190    }
2191
2192    /**
2193     * Remove the connection callbacks (PacketListener, PacketInterceptor, StanzaCollector) used by this MUC from the
2194     * connection.
2195     */
2196    private void removeConnectionCallbacks() {
2197        connection.removeStanzaListener(messageListener);
2198        connection.removeStanzaListener(presenceListener);
2199        connection.removeStanzaListener(subjectListener);
2200        connection.removeStanzaListener(declinesListener);
2201        connection.removeStanzaSendingListener(presenceInterceptor);
2202        if (messageCollector != null) {
2203            messageCollector.cancel();
2204            messageCollector = null;
2205        }
2206    }
2207
2208    /**
2209     * Remove all callbacks and resources necessary when the user has left the room for some reason.
2210     */
2211    private synchronized void userHasLeft() {
2212        // We do not reset nickname here, in case this method has been called erroneously, it should still be possible
2213        // to call leave() in order to resync the state. And leave() requires the nickname to send the unsubscribe
2214        // presence.
2215        occupantsMap.clear();
2216        myRoomJid = null;
2217        // Update the list of joined rooms
2218        multiUserChatManager.removeJoinedRoom(room);
2219        removeConnectionCallbacks();
2220    }
2221
2222    /**
2223     * Adds a listener that will be notified of changes in your status in the room
2224     * such as the user being kicked, banned, or granted admin permissions.
2225     *
2226     * @param listener a user status listener.
2227     * @return true if the user status listener was not already added.
2228     */
2229    public boolean addUserStatusListener(UserStatusListener listener) {
2230        return userStatusListeners.add(listener);
2231    }
2232
2233    /**
2234     * Removes a listener that was being notified of changes in your status in the room
2235     * such as the user being kicked, banned, or granted admin permissions.
2236     *
2237     * @param listener a user status listener.
2238     * @return true if the listener was registered and is now removed.
2239     */
2240    public boolean removeUserStatusListener(UserStatusListener listener) {
2241        return userStatusListeners.remove(listener);
2242    }
2243
2244    /**
2245     * Adds a listener that will be notified of changes in occupants status in the room
2246     * such as the user being kicked, banned, or granted admin permissions.
2247     *
2248     * @param listener a participant status listener.
2249     * @return true if the listener was not already added.
2250     */
2251    public boolean addParticipantStatusListener(ParticipantStatusListener listener) {
2252        return participantStatusListeners.add(listener);
2253    }
2254
2255    /**
2256     * Removes a listener that was being notified of changes in occupants status in the room
2257     * such as the user being kicked, banned, or granted admin permissions.
2258     *
2259     * @param listener a participant status listener.
2260     * @return true if the listener was registered and is now removed.
2261     */
2262    public boolean removeParticipantStatusListener(ParticipantStatusListener listener) {
2263        return participantStatusListeners.remove(listener);
2264    }
2265
2266    /**
2267     * Fires notification events if the role of a room occupant has changed. If the occupant that
2268     * changed his role is your occupant then the <code>UserStatusListeners</code> added to this
2269     * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
2270     * his role is not yours then the <code>ParticipantStatusListeners</code> added to this
2271     * <code>MultiUserChat</code> will be fired. The following table shows the events that will
2272     * be fired depending on the previous and new role of the occupant.
2273     *
2274     * <pre>
2275     * <table border="1">
2276     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
2277     *
2278     * <tr><td>None</td><td>Visitor</td><td>--</td></tr>
2279     * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
2280     * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
2281     *
2282     * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
2283     * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
2284     * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
2285     *
2286     * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
2287     * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
2288     * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
2289     *
2290     * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
2291     * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
2292     * <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
2293     * </table>
2294     * </pre>
2295     *
2296     * @param oldRole the previous role of the user in the room before receiving the new presence
2297     * @param newRole the new role of the user in the room after receiving the new presence
2298     * @param isUserModification whether the received presence is about your user in the room or not
2299     * @param from the occupant whose role in the room has changed
2300     * (e.g. room@conference.jabber.org/nick).
2301     */
2302    private void checkRoleModifications(
2303        MUCRole oldRole,
2304        MUCRole newRole,
2305        boolean isUserModification,
2306        EntityFullJid from) {
2307        // Voice was granted to a visitor
2308        if ((MUCRole.visitor.equals(oldRole) || MUCRole.none.equals(oldRole))
2309            && MUCRole.participant.equals(newRole)) {
2310            if (isUserModification) {
2311                for (UserStatusListener listener : userStatusListeners) {
2312                    listener.voiceGranted();
2313                }
2314            }
2315            else {
2316                for (ParticipantStatusListener listener : participantStatusListeners) {
2317                    listener.voiceGranted(from);
2318                }
2319            }
2320        }
2321        // The participant's voice was revoked from the room
2322        else if (
2323            MUCRole.participant.equals(oldRole)
2324                && (MUCRole.visitor.equals(newRole) || MUCRole.none.equals(newRole))) {
2325            if (isUserModification) {
2326                for (UserStatusListener listener : userStatusListeners) {
2327                    listener.voiceRevoked();
2328                }
2329            }
2330            else {
2331                for (ParticipantStatusListener listener : participantStatusListeners) {
2332                    listener.voiceRevoked(from);
2333                }
2334            }
2335        }
2336        // Moderator privileges were granted to a participant
2337        if (!MUCRole.moderator.equals(oldRole) && MUCRole.moderator.equals(newRole)) {
2338            if (MUCRole.visitor.equals(oldRole) || MUCRole.none.equals(oldRole)) {
2339                if (isUserModification) {
2340                    for (UserStatusListener listener : userStatusListeners) {
2341                        listener.voiceGranted();
2342                    }
2343                }
2344                else {
2345                    for (ParticipantStatusListener listener : participantStatusListeners) {
2346                        listener.voiceGranted(from);
2347                    }
2348                }
2349            }
2350            if (isUserModification) {
2351                for (UserStatusListener listener : userStatusListeners) {
2352                    listener.moderatorGranted();
2353                }
2354            }
2355            else {
2356                for (ParticipantStatusListener listener : participantStatusListeners) {
2357                    listener.moderatorGranted(from);
2358                }
2359            }
2360        }
2361        // Moderator privileges were revoked from a participant
2362        else if (MUCRole.moderator.equals(oldRole) && !MUCRole.moderator.equals(newRole)) {
2363            if (MUCRole.visitor.equals(newRole) || MUCRole.none.equals(newRole)) {
2364                if (isUserModification) {
2365                    for (UserStatusListener listener : userStatusListeners) {
2366                        listener.voiceRevoked();
2367                    }
2368                }
2369                else {
2370                    for (ParticipantStatusListener listener : participantStatusListeners) {
2371                        listener.voiceRevoked(from);
2372                    }
2373                }
2374            }
2375            if (isUserModification) {
2376                for (UserStatusListener listener : userStatusListeners) {
2377                    listener.moderatorRevoked();
2378                }
2379            }
2380            else {
2381                for (ParticipantStatusListener listener : participantStatusListeners) {
2382                    listener.moderatorRevoked(from);
2383                }
2384            }
2385        }
2386    }
2387
2388    /**
2389     * Fires notification events if the affiliation of a room occupant has changed. If the
2390     * occupant that changed his affiliation is your occupant then the
2391     * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
2392     * On the other hand, if the occupant that changed his affiliation is not yours then the
2393     * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
2394     * fired. The following table shows the events that will be fired depending on the previous
2395     * and new affiliation of the occupant.
2396     *
2397     * <pre>
2398     * <table border="1">
2399     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
2400     *
2401     * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
2402     * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
2403     * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
2404     *
2405     * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
2406     * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
2407     * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
2408     *
2409     * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
2410     * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
2411     * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
2412     *
2413     * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
2414     * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
2415     * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
2416     * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
2417     * </table>
2418     * </pre>
2419     *
2420     * @param oldAffiliation the previous affiliation of the user in the room before receiving the
2421     * new presence
2422     * @param newAffiliation the new affiliation of the user in the room after receiving the new
2423     * presence
2424     * @param isUserModification whether the received presence is about your user in the room or not
2425     * @param from the occupant whose role in the room has changed
2426     * (e.g. room@conference.jabber.org/nick).
2427     */
2428    private void checkAffiliationModifications(
2429        MUCAffiliation oldAffiliation,
2430        MUCAffiliation newAffiliation,
2431        boolean isUserModification,
2432        EntityFullJid from) {
2433        // First check for revoked affiliation and then for granted affiliations. The idea is to
2434        // first fire the "revoke" events and then fire the "grant" events.
2435
2436        // The user's ownership to the room was revoked
2437        if (MUCAffiliation.owner.equals(oldAffiliation) && !MUCAffiliation.owner.equals(newAffiliation)) {
2438            if (isUserModification) {
2439                for (UserStatusListener listener : userStatusListeners) {
2440                    listener.ownershipRevoked();
2441                }
2442            }
2443            else {
2444                for (ParticipantStatusListener listener : participantStatusListeners) {
2445                    listener.ownershipRevoked(from);
2446                }
2447            }
2448        }
2449        // The user's administrative privileges to the room were revoked
2450        else if (MUCAffiliation.admin.equals(oldAffiliation) && !MUCAffiliation.admin.equals(newAffiliation)) {
2451            if (isUserModification) {
2452                for (UserStatusListener listener : userStatusListeners) {
2453                    listener.adminRevoked();
2454                }
2455            }
2456            else {
2457                for (ParticipantStatusListener listener : participantStatusListeners) {
2458                    listener.adminRevoked(from);
2459                }
2460            }
2461        }
2462        // The user's membership to the room was revoked
2463        else if (MUCAffiliation.member.equals(oldAffiliation) && !MUCAffiliation.member.equals(newAffiliation)) {
2464            if (isUserModification) {
2465                for (UserStatusListener listener : userStatusListeners) {
2466                    listener.membershipRevoked();
2467                }
2468            }
2469            else {
2470                for (ParticipantStatusListener listener : participantStatusListeners) {
2471                    listener.membershipRevoked(from);
2472                }
2473            }
2474        }
2475
2476        // The user was granted ownership to the room
2477        if (!MUCAffiliation.owner.equals(oldAffiliation) && MUCAffiliation.owner.equals(newAffiliation)) {
2478            if (isUserModification) {
2479                for (UserStatusListener listener : userStatusListeners) {
2480                    listener.ownershipGranted();
2481                }
2482            }
2483            else {
2484                for (ParticipantStatusListener listener : participantStatusListeners) {
2485                    listener.ownershipGranted(from);
2486                }
2487            }
2488        }
2489        // The user was granted administrative privileges to the room
2490        else if (!MUCAffiliation.admin.equals(oldAffiliation) && MUCAffiliation.admin.equals(newAffiliation)) {
2491            if (isUserModification) {
2492                for (UserStatusListener listener : userStatusListeners) {
2493                    listener.adminGranted();
2494                }
2495            }
2496            else {
2497                for (ParticipantStatusListener listener : participantStatusListeners) {
2498                    listener.adminGranted(from);
2499                }
2500            }
2501        }
2502        // The user was granted membership to the room
2503        else if (!MUCAffiliation.member.equals(oldAffiliation) && MUCAffiliation.member.equals(newAffiliation)) {
2504            if (isUserModification) {
2505                for (UserStatusListener listener : userStatusListeners) {
2506                    listener.membershipGranted();
2507                }
2508            }
2509            else {
2510                for (ParticipantStatusListener listener : participantStatusListeners) {
2511                    listener.membershipGranted(from);
2512                }
2513            }
2514        }
2515    }
2516
2517    /**
2518     * Fires events according to the received presence code.
2519     *
2520     * @param statusCodes TODO javadoc me please
2521     * @param isUserModification TODO javadoc me please
2522     * @param mucUser TODO javadoc me please
2523     * @param from TODO javadoc me please
2524     */
2525    private void checkPresenceCode(
2526        Set<Status> statusCodes,
2527        boolean isUserModification,
2528        MUCUser mucUser,
2529        EntityFullJid from) {
2530        // Check if an occupant was kicked from the room
2531        if (statusCodes.contains(Status.KICKED_307)) {
2532            // Check if this occupant was kicked
2533            if (isUserModification) {
2534                for (UserStatusListener listener : userStatusListeners) {
2535                    listener.kicked(mucUser.getItem().getActor(), mucUser.getItem().getReason());
2536                }
2537            }
2538            else {
2539                for (ParticipantStatusListener listener : participantStatusListeners) {
2540                    listener.kicked(from, mucUser.getItem().getActor(), mucUser.getItem().getReason());
2541                }
2542            }
2543        }
2544        // A user was banned from the room
2545        if (statusCodes.contains(Status.BANNED_301)) {
2546            // Check if this occupant was banned
2547            if (isUserModification) {
2548                for (UserStatusListener listener : userStatusListeners) {
2549                    listener.banned(mucUser.getItem().getActor(), mucUser.getItem().getReason());
2550                }
2551            }
2552            else {
2553                for (ParticipantStatusListener listener : participantStatusListeners) {
2554                    listener.banned(from, mucUser.getItem().getActor(), mucUser.getItem().getReason());
2555                }
2556            }
2557        }
2558        // A user's membership was revoked from the room
2559        if (statusCodes.contains(Status.REMOVED_AFFIL_CHANGE_321)) {
2560            // Check if this occupant's membership was revoked
2561            if (isUserModification) {
2562                for (UserStatusListener listener : userStatusListeners) {
2563                    listener.membershipRevoked();
2564                }
2565            }
2566        }
2567        // A occupant has changed his nickname in the room
2568        if (statusCodes.contains(Status.NEW_NICKNAME_303)) {
2569            for (ParticipantStatusListener listener : participantStatusListeners) {
2570                listener.nicknameChanged(from, mucUser.getItem().getNick());
2571            }
2572        }
2573    }
2574
2575    /**
2576     * Get the XMPP connection associated with this chat instance.
2577     *
2578     * @return the associated XMPP connection.
2579     * @since 4.3.0
2580     */
2581    public XMPPConnection getXmppConnection() {
2582        return connection;
2583    }
2584
2585    public boolean serviceSupportsStableIds() {
2586        return DiscoverInfo.nullSafeContainsFeature(mucServiceDiscoInfo, MultiUserChatConstants.STABLE_ID_FEATURE);
2587    }
2588
2589    @Override
2590    public String toString() {
2591        return "MUC: " + room + "(" + connection.getUser() + ")";
2592    }
2593}