MultiUserChat.java

  1. /**
  2.  *
  3.  * Copyright 2003-2007 Jive Software. 2020-2024 Florian Schmaus
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.jivesoftware.smackx.muc;

  18. import java.util.ArrayList;
  19. import java.util.Collection;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.Set;
  23. import java.util.concurrent.ConcurrentHashMap;
  24. import java.util.concurrent.CopyOnWriteArrayList;
  25. import java.util.concurrent.CopyOnWriteArraySet;
  26. import java.util.concurrent.atomic.AtomicInteger;
  27. import java.util.logging.Level;
  28. import java.util.logging.Logger;

  29. import org.jivesoftware.smack.MessageListener;
  30. import org.jivesoftware.smack.PresenceListener;
  31. import org.jivesoftware.smack.SmackException;
  32. import org.jivesoftware.smack.SmackException.NoResponseException;
  33. import org.jivesoftware.smack.SmackException.NotConnectedException;
  34. import org.jivesoftware.smack.StanzaCollector;
  35. import org.jivesoftware.smack.StanzaListener;
  36. import org.jivesoftware.smack.XMPPConnection;
  37. import org.jivesoftware.smack.XMPPException;
  38. import org.jivesoftware.smack.XMPPException.XMPPErrorException;
  39. import org.jivesoftware.smack.chat.ChatMessageListener;
  40. import org.jivesoftware.smack.filter.AndFilter;
  41. import org.jivesoftware.smack.filter.FromMatchesFilter;
  42. import org.jivesoftware.smack.filter.MessageTypeFilter;
  43. import org.jivesoftware.smack.filter.MessageWithBodiesFilter;
  44. import org.jivesoftware.smack.filter.MessageWithSubjectFilter;
  45. import org.jivesoftware.smack.filter.MessageWithThreadFilter;
  46. import org.jivesoftware.smack.filter.NotFilter;
  47. import org.jivesoftware.smack.filter.OrFilter;
  48. import org.jivesoftware.smack.filter.PossibleFromTypeFilter;
  49. import org.jivesoftware.smack.filter.PresenceTypeFilter;
  50. import org.jivesoftware.smack.filter.StanzaExtensionFilter;
  51. import org.jivesoftware.smack.filter.StanzaFilter;
  52. import org.jivesoftware.smack.filter.StanzaIdFilter;
  53. import org.jivesoftware.smack.filter.StanzaTypeFilter;
  54. import org.jivesoftware.smack.filter.ToMatchesFilter;
  55. import org.jivesoftware.smack.packet.IQ;
  56. import org.jivesoftware.smack.packet.Message;
  57. import org.jivesoftware.smack.packet.MessageBuilder;
  58. import org.jivesoftware.smack.packet.MessageView;
  59. import org.jivesoftware.smack.packet.Presence;
  60. import org.jivesoftware.smack.packet.PresenceBuilder;
  61. import org.jivesoftware.smack.packet.Stanza;
  62. import org.jivesoftware.smack.util.Consumer;
  63. import org.jivesoftware.smack.util.Objects;

  64. import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
  65. import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
  66. import org.jivesoftware.smackx.iqregister.packet.Registration;
  67. import org.jivesoftware.smackx.muc.MultiUserChatException.MissingMucCreationAcknowledgeException;
  68. import org.jivesoftware.smackx.muc.MultiUserChatException.MucAlreadyJoinedException;
  69. import org.jivesoftware.smackx.muc.MultiUserChatException.MucNotJoinedException;
  70. import org.jivesoftware.smackx.muc.MultiUserChatException.NotAMucServiceException;
  71. import org.jivesoftware.smackx.muc.filter.MUCUserStatusCodeFilter;
  72. import org.jivesoftware.smackx.muc.packet.Destroy;
  73. import org.jivesoftware.smackx.muc.packet.GroupChatInvitation;
  74. import org.jivesoftware.smackx.muc.packet.MUCAdmin;
  75. import org.jivesoftware.smackx.muc.packet.MUCInitialPresence;
  76. import org.jivesoftware.smackx.muc.packet.MUCItem;
  77. import org.jivesoftware.smackx.muc.packet.MUCOwner;
  78. import org.jivesoftware.smackx.muc.packet.MUCUser;
  79. import org.jivesoftware.smackx.muc.packet.MUCUser.Status;
  80. import org.jivesoftware.smackx.xdata.FormField;
  81. import org.jivesoftware.smackx.xdata.TextSingleFormField;
  82. import org.jivesoftware.smackx.xdata.form.FillableForm;
  83. import org.jivesoftware.smackx.xdata.form.Form;
  84. import org.jivesoftware.smackx.xdata.packet.DataForm;

  85. import org.jxmpp.jid.BareJid;
  86. import org.jxmpp.jid.DomainBareJid;
  87. import org.jxmpp.jid.EntityBareJid;
  88. import org.jxmpp.jid.EntityFullJid;
  89. import org.jxmpp.jid.Jid;
  90. import org.jxmpp.jid.impl.JidCreate;
  91. import org.jxmpp.jid.parts.Resourcepart;

  92. /**
  93.  * A MultiUserChat room (XEP-45), created with {@link MultiUserChatManager#getMultiUserChat(EntityBareJid)}.
  94.  * <p>
  95.  * A MultiUserChat is a conversation that takes place among many users in a virtual
  96.  * room. A room could have many occupants with different affiliation and roles.
  97.  * Possible affiliations are "owner", "admin", "member", and "outcast". Possible roles
  98.  * are "moderator", "participant", and "visitor". Each role and affiliation guarantees
  99.  * different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
  100.  * Grant voice, Edit member list, etc.).
  101.  * </p>
  102.  * <p>
  103.  * <b>Note:</b> Make sure to leave the MUC ({@link #leave()}) when you don't need it anymore or
  104.  * otherwise you may leak the instance.
  105.  * </p>
  106.  *
  107.  * @author Gaston Dombiak
  108.  * @author Larry Kirschner
  109.  * @author Florian Schmaus
  110.  */
  111. public class MultiUserChat {
  112.     private static final Logger LOGGER = Logger.getLogger(MultiUserChat.class.getName());

  113.     private final XMPPConnection connection;
  114.     private final EntityBareJid room;
  115.     private final MultiUserChatManager multiUserChatManager;
  116.     private final Map<EntityFullJid, Presence> occupantsMap = new ConcurrentHashMap<>();

  117.     private final Set<InvitationRejectionListener> invitationRejectionListeners = new CopyOnWriteArraySet<InvitationRejectionListener>();
  118.     private final Set<SubjectUpdatedListener> subjectUpdatedListeners = new CopyOnWriteArraySet<SubjectUpdatedListener>();
  119.     private final Set<UserStatusListener> userStatusListeners = new CopyOnWriteArraySet<UserStatusListener>();
  120.     private final Set<ParticipantStatusListener> participantStatusListeners = new CopyOnWriteArraySet<ParticipantStatusListener>();
  121.     private final Set<MessageListener> messageListeners = new CopyOnWriteArraySet<MessageListener>();
  122.     private final Set<PresenceListener> presenceListeners = new CopyOnWriteArraySet<PresenceListener>();
  123.     private final Set<Consumer<PresenceBuilder>> presenceInterceptors = new CopyOnWriteArraySet<>();

  124.     /**
  125.      * This filter will match all stanzas send from the groupchat or from one if
  126.      * the groupchat participants, i.e. it filters only the bare JID of the from
  127.      * attribute against the JID of the MUC.
  128.      */
  129.     private final StanzaFilter fromRoomFilter;

  130.     /**
  131.      * Same as {@link #fromRoomFilter} together with {@link MessageTypeFilter#GROUPCHAT}.
  132.      */
  133.     private final StanzaFilter fromRoomGroupchatFilter;

  134.     private final AtomicInteger presenceInterceptorCount = new AtomicInteger();
  135.     // We want to save the presence interceptor in a variable, using a lambda, (and not use a method reference) to be
  136.     // able to dynamically add and remove it from the connection.
  137.     @SuppressWarnings("UnnecessaryLambda")
  138.     private final Consumer<PresenceBuilder> presenceInterceptor = presenceBuilder -> {
  139.         for (Consumer<PresenceBuilder> interceptor : presenceInterceptors) {
  140.             interceptor.accept(presenceBuilder);
  141.         }
  142.     };

  143.     private final StanzaListener messageListener;
  144.     private final StanzaListener presenceListener;
  145.     private final StanzaListener subjectListener;

  146.     private static final StanzaFilter DECLINE_FILTER = new AndFilter(MessageTypeFilter.NORMAL,
  147.                     new StanzaExtensionFilter(MUCUser.ELEMENT, MUCUser.NAMESPACE));
  148.     private final StanzaListener declinesListener;

  149.     private String subject;
  150.     private EntityFullJid myRoomJid;
  151.     private StanzaCollector messageCollector;

  152.     private DiscoverInfo mucServiceDiscoInfo;

  153.     /**
  154.      * Used to signal that the reflected self-presence was received <b>and</b> processed by us.
  155.      */
  156.     private volatile boolean processedReflectedSelfPresence;

  157.     private CopyOnWriteArrayList<MucMessageInterceptor> messageInterceptors;

  158.     MultiUserChat(XMPPConnection connection, EntityBareJid room, MultiUserChatManager multiUserChatManager) {
  159.         this.connection = connection;
  160.         this.room = room;
  161.         this.multiUserChatManager = multiUserChatManager;
  162.         this.messageInterceptors = MultiUserChatManager.getMessageInterceptors();

  163.         fromRoomFilter = FromMatchesFilter.create(room);
  164.         fromRoomGroupchatFilter = new AndFilter(fromRoomFilter, MessageTypeFilter.GROUPCHAT);

  165.         messageListener = new StanzaListener() {
  166.             @Override
  167.             public void processStanza(Stanza packet) throws NotConnectedException {
  168.                 final Message message = (Message) packet;

  169.                 for (MessageListener listener : messageListeners) {
  170.                             listener.processMessage(message);
  171.                 }
  172.             }
  173.         };

  174.         // Create a listener for subject updates.
  175.         subjectListener = new StanzaListener() {
  176.             @Override
  177.             public void processStanza(Stanza packet) {
  178.                 final Message msg = (Message) packet;
  179.                 final EntityFullJid from = msg.getFrom().asEntityFullJidIfPossible();
  180.                 // Update the room subject
  181.                 subject = msg.getSubject();

  182.                 // Fire event for subject updated listeners
  183.                 for (SubjectUpdatedListener listener : subjectUpdatedListeners) {
  184.                     listener.subjectUpdated(msg.getSubject(), from);
  185.                 }
  186.             }
  187.         };

  188.         // Create a listener for all presence updates.
  189.         presenceListener = new StanzaListener() {
  190.             @Override
  191.             public void processStanza(final Stanza packet) {
  192.                 final Presence presence = (Presence) packet;
  193.                 final EntityFullJid from = presence.getFrom().asEntityFullJidIfPossible();
  194.                 if (from == null) {
  195.                     return;
  196.                 }
  197.                 final EntityFullJid myRoomJID = getMyRoomJid();
  198.                 final boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
  199.                 final MUCUser mucUser = MUCUser.from(packet);

  200.                 switch (presence.getType()) {
  201.                 case available:
  202.                     if (!processedReflectedSelfPresence
  203.                                     && mucUser.getStatus().contains(MUCUser.Status.PRESENCE_TO_SELF_110)) {
  204.                         processedReflectedSelfPresence = true;
  205.                         synchronized (this) {
  206.                             notify();
  207.                         }
  208.                     }

  209.                     Presence oldPresence = occupantsMap.put(from, presence);
  210.                     if (oldPresence != null) {
  211.                         // Get the previous occupant's affiliation & role
  212.                         MUCUser mucExtension = MUCUser.from(oldPresence);
  213.                         MUCAffiliation oldAffiliation = mucExtension.getItem().getAffiliation();
  214.                         MUCRole oldRole = mucExtension.getItem().getRole();
  215.                         // Get the new occupant's affiliation & role
  216.                         MUCAffiliation newAffiliation = mucUser.getItem().getAffiliation();
  217.                         MUCRole newRole = mucUser.getItem().getRole();
  218.                         // Fire role modification events
  219.                         checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
  220.                         // Fire affiliation modification events
  221.                         checkAffiliationModifications(
  222.                             oldAffiliation,
  223.                             newAffiliation,
  224.                             isUserStatusModification,
  225.                             from);
  226.                     } else {
  227.                         // A new occupant has joined the room
  228.                         for (ParticipantStatusListener listener : participantStatusListeners) {
  229.                             listener.joined(from);
  230.                         }
  231.                     }
  232.                     break;
  233.                 case unavailable:
  234.                     occupantsMap.remove(from);
  235.                     Set<Status> status = mucUser.getStatus();
  236.                     if (mucUser != null && !status.isEmpty()) {
  237.                         if (isUserStatusModification && !status.contains(MUCUser.Status.NEW_NICKNAME_303)) {
  238.                             userHasLeft();
  239.                         }
  240.                         // Fire events according to the received presence code
  241.                         checkPresenceCode(
  242.                             status,
  243.                             isUserStatusModification,
  244.                             mucUser,
  245.                             from);
  246.                     } else {
  247.                         // An occupant has left the room
  248.                         if (!isUserStatusModification) {
  249.                             for (ParticipantStatusListener listener : participantStatusListeners) {
  250.                                 listener.left(from);
  251.                             }
  252.                         }
  253.                     }

  254.                     Destroy destroy = mucUser == null ? null : mucUser.getDestroy();
  255.                     // The room has been destroyed.
  256.                     if (destroy != null) {
  257.                         EntityBareJid alternateMucJid = destroy.getJid();
  258.                         final MultiUserChat alternateMuc;
  259.                         if (alternateMucJid == null) {
  260.                             alternateMuc = null;
  261.                         } else {
  262.                             alternateMuc = multiUserChatManager.getMultiUserChat(alternateMucJid);
  263.                         }

  264.                         for (UserStatusListener listener : userStatusListeners) {
  265.                             listener.roomDestroyed(alternateMuc, destroy.getPassword(), destroy.getReason());
  266.                         }
  267.                         userHasLeft();
  268.                     }

  269.                     if (isUserStatusModification) {
  270.                         for (UserStatusListener listener : userStatusListeners) {
  271.                             listener.removed(mucUser, presence);
  272.                         }
  273.                     } else {
  274.                         for (ParticipantStatusListener listener : participantStatusListeners) {
  275.                             listener.parted(from);
  276.                         }
  277.                     }
  278.                     break;
  279.                 default:
  280.                     break;
  281.                 }
  282.                 for (PresenceListener listener : presenceListeners) {
  283.                     listener.processPresence(presence);
  284.                 }
  285.             }
  286.         };

  287.         // Listens for all messages that include a MUCUser extension and fire the invitation
  288.         // rejection listeners if the message includes an invitation rejection.
  289.         declinesListener = new StanzaListener() {
  290.             @Override
  291.             public void processStanza(Stanza packet) {
  292.                 Message message = (Message) packet;
  293.                 // Get the MUC User extension
  294.                 MUCUser mucUser = MUCUser.from(packet);
  295.                 MUCUser.Decline rejection = mucUser.getDecline();
  296.                 // Check if the MUCUser informs that the invitee has declined the invitation
  297.                 if (rejection == null) {
  298.                     return;
  299.                 }
  300.                 // Fire event for invitation rejection listeners
  301.                 fireInvitationRejectionListeners(message, rejection);
  302.             }
  303.         };
  304.     }


  305.     /**
  306.      * Returns the name of the room this MultiUserChat object represents.
  307.      *
  308.      * @return the multi user chat room name.
  309.      */
  310.     public EntityBareJid getRoom() {
  311.         return room;
  312.     }

  313.     /**
  314.      * Enter a room, as described in XEP-45 7.2.
  315.      *
  316.      * @param conf the configuration used to enter the room.
  317.      * @return the returned presence by the service after the client send the initial presence in order to enter the room.
  318.      * @throws NotConnectedException if the XMPP connection is not connected.
  319.      * @throws NoResponseException if there was no response from the remote entity.
  320.      * @throws XMPPErrorException if there was an XMPP error returned.
  321.      * @throws InterruptedException if the calling thread was interrupted.
  322.      * @throws NotAMucServiceException if the entity is not a MUC service.
  323.      * @see <a href="http://xmpp.org/extensions/xep-0045.html#enter">XEP-45 7.2 Entering a Room</a>
  324.      */
  325.     private Presence enter(MucEnterConfiguration conf) throws NotConnectedException, NoResponseException,
  326.                     XMPPErrorException, InterruptedException, NotAMucServiceException {
  327.         final DomainBareJid mucService = room.asDomainBareJid();
  328.         mucServiceDiscoInfo = multiUserChatManager.getMucServiceDiscoInfo(mucService);
  329.         if (mucServiceDiscoInfo == null) {
  330.             throw new NotAMucServiceException(this);
  331.         }
  332.         // We enter a room by sending a presence packet where the "to"
  333.         // field is in the form "roomName@service/nickname"
  334.         Presence joinPresence = conf.getJoinPresence(this);

  335.         // Set up the messageListeners and presenceListeners *before* the join presence is sent.
  336.         connection.addStanzaListener(messageListener, fromRoomGroupchatFilter);
  337.         StanzaFilter presenceFromRoomFilter = new AndFilter(fromRoomFilter,
  338.                         StanzaTypeFilter.PRESENCE,
  339.                         PossibleFromTypeFilter.ENTITY_FULL_JID);
  340.         connection.addStanzaListener(presenceListener, presenceFromRoomFilter);
  341.         // @formatter:off
  342.         connection.addStanzaListener(subjectListener,
  343.                         new AndFilter(fromRoomFilter,
  344.                                       MessageWithSubjectFilter.INSTANCE,
  345.                                       new NotFilter(MessageTypeFilter.ERROR),
  346.                                       // According to XEP-0045 § 8.1 "A message with a <subject/> and a <body/> or a <subject/> and a <thread/> is a
  347.                                       // legitimate message, but it SHALL NOT be interpreted as a subject change."
  348.                                       new NotFilter(MessageWithBodiesFilter.INSTANCE),
  349.                                       new NotFilter(MessageWithThreadFilter.INSTANCE))
  350.                         );
  351.         // @formatter:on
  352.         connection.addStanzaListener(declinesListener, new AndFilter(fromRoomFilter, DECLINE_FILTER));
  353.         messageCollector = connection.createStanzaCollector(fromRoomGroupchatFilter);

  354.         // Wait for a presence packet back from the server.
  355.         // @formatter:off
  356.         StanzaFilter responseFilter = new AndFilter(StanzaTypeFilter.PRESENCE,
  357.                         new OrFilter(
  358.                             // We use a bare JID filter for positive responses, since the MUC service/room may rewrite the nickname.
  359.                             new AndFilter(FromMatchesFilter.createBare(getRoom()), MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF),
  360.                             // In case there is an error reply, we match on an error presence with the same stanza id and from the full
  361.                             // JID we send the join presence to.
  362.                             new AndFilter(FromMatchesFilter.createFull(joinPresence.getTo()), new StanzaIdFilter(joinPresence), PresenceTypeFilter.ERROR)
  363.                         )
  364.                     );
  365.         // @formatter:on
  366.         processedReflectedSelfPresence = false;
  367.         StanzaCollector presenceStanzaCollector = null;
  368.         final Presence reflectedSelfPresence;
  369.         try {
  370.             // This stanza collector will collect the final self presence from the MUC, which also signals that we have successfully entered the MUC.
  371.             StanzaCollector selfPresenceCollector = connection.createStanzaCollectorAndSend(responseFilter, joinPresence);
  372.             StanzaCollector.Configuration presenceStanzaCollectorConfiguration = StanzaCollector.newConfiguration().setCollectorToReset(
  373.                             selfPresenceCollector).setStanzaFilter(presenceFromRoomFilter);
  374.             // This stanza collector is used to reset the timeout of the selfPresenceCollector.
  375.             presenceStanzaCollector = connection.createStanzaCollector(presenceStanzaCollectorConfiguration);
  376.             reflectedSelfPresence = selfPresenceCollector.nextResultOrThrow(conf.getTimeout());
  377.         }
  378.         catch (NotConnectedException | InterruptedException | NoResponseException | XMPPErrorException e) {
  379.             // Ensure that all callbacks are removed if there is an exception
  380.             removeConnectionCallbacks();
  381.             throw e;
  382.         }
  383.         finally {
  384.             if (presenceStanzaCollector != null) {
  385.                 presenceStanzaCollector.cancel();
  386.             }
  387.         }

  388.         synchronized (presenceListener) {
  389.             // Only continue after we have received *and* processed the reflected self-presence. Since presences are
  390.             // handled in an extra listener, we may return from enter() without having processed all presences of the
  391.             // participants, resulting in a e.g. to low participant counter after enter(). Hence, we wait here until the
  392.             // processing is done.
  393.             while (!processedReflectedSelfPresence) {
  394.                 presenceListener.wait();
  395.             }
  396.         }

  397.         // This presence must be sent from a full JID. We use the resourcepart of this JID as nick, since the room may
  398.         // have performed roomnick rewriting
  399.         Resourcepart receivedNickname = reflectedSelfPresence.getFrom().getResourceOrThrow();
  400.         setNickname(receivedNickname);

  401.         // Update the list of joined rooms
  402.         multiUserChatManager.addJoinedRoom(room);
  403.         return reflectedSelfPresence;
  404.     }

  405.     private void setNickname(Resourcepart nickname) {
  406.         this.myRoomJid = JidCreate.entityFullFrom(room, nickname);
  407.     }

  408.     /**
  409.      * Get a new MUC enter configuration builder.
  410.      *
  411.      * @param nickname the nickname used when entering the MUC room.
  412.      * @return a new MUC enter configuration builder.
  413.      * @since 4.2
  414.      */
  415.     public MucEnterConfiguration.Builder getEnterConfigurationBuilder(Resourcepart nickname) {
  416.         return new MucEnterConfiguration.Builder(nickname, connection);
  417.     }

  418.     /**
  419.      * Creates the room according to some default configuration, assign the requesting user as the
  420.      * room owner, and add the owner to the room but not allow anyone else to enter the room
  421.      * (effectively "locking" the room). The requesting user will join the room under the specified
  422.      * nickname as soon as the room has been created.
  423.      * <p>
  424.      * To create an "Instant Room", that means a room with some default configuration that is
  425.      * available for immediate access, the room's owner should send an empty form after creating the
  426.      * room. Simply call {@link MucCreateConfigFormHandle#makeInstant()} on the returned {@link MucCreateConfigFormHandle}.
  427.      * </p>
  428.      * <p>
  429.      * To create a "Reserved Room", that means a room manually configured by the room creator before
  430.      * anyone is allowed to enter, the room's owner should complete and send a form after creating
  431.      * the room. Once the completed configuration form is sent to the server, the server will unlock
  432.      * the room. You can use the returned {@link MucCreateConfigFormHandle} to configure the room.
  433.      * </p>
  434.      *
  435.      * @param nickname the nickname to use.
  436.      * @return a handle to the MUC create configuration form API.
  437.      * @throws XMPPErrorException if the room couldn't be created for some reason (e.g. 405 error if
  438.      *         the user is not allowed to create the room)
  439.      * @throws NoResponseException if there was no response from the server.
  440.      * @throws InterruptedException if the calling thread was interrupted.
  441.      * @throws NotConnectedException if the XMPP connection is not connected.
  442.      * @throws MucAlreadyJoinedException if already joined the Multi-User Chat.7y
  443.      * @throws MissingMucCreationAcknowledgeException if there MUC creation was not acknowledged by the service.
  444.      * @throws NotAMucServiceException if the entity is not a MUC service.
  445.      */
  446.     public synchronized MucCreateConfigFormHandle create(Resourcepart nickname) throws NoResponseException,
  447.                     XMPPErrorException, InterruptedException, MucAlreadyJoinedException,
  448.                     NotConnectedException, MissingMucCreationAcknowledgeException, NotAMucServiceException {
  449.         if (isJoined()) {
  450.             throw new MucAlreadyJoinedException();
  451.         }

  452.         MucCreateConfigFormHandle mucCreateConfigFormHandle = createOrJoin(nickname);
  453.         if (mucCreateConfigFormHandle != null) {
  454.             // We successfully created a new room
  455.             return mucCreateConfigFormHandle;
  456.         }
  457.         // We need to leave the room since it seems that the room already existed
  458.         try {
  459.             leave();
  460.         }
  461.         catch (MucNotJoinedException e) {
  462.             LOGGER.log(Level.INFO, "Unexpected MucNotJoinedException", e);
  463.         }
  464.         throw new MissingMucCreationAcknowledgeException();
  465.     }

  466.     /**
  467.      * Create or join the MUC room with the given nickname.
  468.      *
  469.      * @param nickname the nickname to use in the MUC room.
  470.      * @return A {@link MucCreateConfigFormHandle} if the room was created while joining, or {@code null} if the room was just joined.
  471.      * @throws NoResponseException if there was no response from the remote entity.
  472.      * @throws XMPPErrorException if there was an XMPP error returned.
  473.      * @throws InterruptedException if the calling thread was interrupted.
  474.      * @throws NotConnectedException if the XMPP connection is not connected.
  475.      * @throws MucAlreadyJoinedException if already joined the Multi-User Chat.7y
  476.      * @throws NotAMucServiceException if the entity is not a MUC service.
  477.      */
  478.     public synchronized MucCreateConfigFormHandle createOrJoin(Resourcepart nickname) throws NoResponseException, XMPPErrorException,
  479.                     InterruptedException, MucAlreadyJoinedException, NotConnectedException, NotAMucServiceException {
  480.         MucEnterConfiguration mucEnterConfiguration = getEnterConfigurationBuilder(nickname).build();
  481.         return createOrJoin(mucEnterConfiguration);
  482.     }

  483.     /**
  484.      * Like {@link #create(Resourcepart)}, but will return a {@link MucCreateConfigFormHandle} if the room creation was acknowledged by
  485.      * the service (with an 201 status code). It's up to the caller to decide, based on the return
  486.      * value, if he needs to continue sending the room configuration. If {@code null} is returned, the room
  487.      * already existed and the user is able to join right away, without sending a form.
  488.      *
  489.      * @param mucEnterConfiguration the configuration used to enter the MUC.
  490.      * @return A {@link MucCreateConfigFormHandle} if the room was created while joining, or {@code null} if the room was just joined.
  491.      * @throws XMPPErrorException if the room couldn't be created for some reason (e.g. 405 error if
  492.      *         the user is not allowed to create the room)
  493.      * @throws NoResponseException if there was no response from the server.
  494.      * @throws InterruptedException if the calling thread was interrupted.
  495.      * @throws MucAlreadyJoinedException if the MUC is already joined
  496.      * @throws NotConnectedException if the XMPP connection is not connected.
  497.      * @throws NotAMucServiceException if the entity is not a MUC service.
  498.      */
  499.     public synchronized MucCreateConfigFormHandle createOrJoin(MucEnterConfiguration mucEnterConfiguration)
  500.                     throws NoResponseException, XMPPErrorException, InterruptedException, MucAlreadyJoinedException, NotConnectedException, NotAMucServiceException {
  501.         if (isJoined()) {
  502.             throw new MucAlreadyJoinedException();
  503.         }

  504.         Presence presence = enter(mucEnterConfiguration);

  505.         // Look for confirmation of room creation from the server
  506.         MUCUser mucUser = MUCUser.from(presence);
  507.         if (mucUser != null && mucUser.getStatus().contains(Status.ROOM_CREATED_201)) {
  508.             // Room was created and the user has joined the room
  509.             return new MucCreateConfigFormHandle();
  510.         }
  511.         return null;
  512.     }

  513.     /**
  514.      * A handle used to configure a newly created room. As long as the room is not configured it will be locked, which
  515.      * means that no one is able to join. The room will become unlocked as soon it got configured. In order to create an
  516.      * instant room, use {@link #makeInstant()}.
  517.      * <p>
  518.      * For advanced configuration options, use {@link MultiUserChat#getConfigurationForm()}, get the answer form with
  519.      * {@link Form#getFillableForm()}, fill it out and send it back to the room with
  520.      * {@link MultiUserChat#sendConfigurationForm(FillableForm)}.
  521.      * </p>
  522.      */
  523.     public class MucCreateConfigFormHandle {

  524.         /**
  525.          * Create an instant room. The default configuration will be accepted and the room will become unlocked, i.e.
  526.          * other users are able to join.
  527.          *
  528.          * @throws NoResponseException if there was no response from the remote entity.
  529.          * @throws XMPPErrorException if there was an XMPP error returned.
  530.          * @throws NotConnectedException if the XMPP connection is not connected.
  531.          * @throws InterruptedException if the calling thread was interrupted.
  532.          * @see <a href="http://www.xmpp.org/extensions/xep-0045.html#createroom-instant">XEP-45 § 10.1.2 Creating an
  533.          *      Instant Room</a>
  534.          */
  535.         public void makeInstant() throws NoResponseException, XMPPErrorException, NotConnectedException,
  536.                         InterruptedException {
  537.             sendConfigurationForm(null);
  538.         }

  539.         /**
  540.          * Alias for {@link MultiUserChat#getConfigFormManager()}.
  541.          *
  542.          * @return a MUC configuration form manager for this room.
  543.          * @throws NoResponseException if there was no response from the remote entity.
  544.          * @throws XMPPErrorException if there was an XMPP error returned.
  545.          * @throws NotConnectedException if the XMPP connection is not connected.
  546.          * @throws InterruptedException if the calling thread was interrupted.
  547.          * @see MultiUserChat#getConfigFormManager()
  548.          */
  549.         public MucConfigFormManager getConfigFormManager() throws NoResponseException,
  550.                         XMPPErrorException, NotConnectedException, InterruptedException {
  551.             return MultiUserChat.this.getConfigFormManager();
  552.         }
  553.     }

  554.     /**
  555.      * Create or join a MUC if it is necessary, i.e. if not the MUC is not already joined.
  556.      *
  557.      * @param nickname the required nickname to use.
  558.      * @param password the optional password required to join
  559.      * @return A {@link MucCreateConfigFormHandle} if the room was created while joining, or {@code null} if the room was just joined.
  560.      * @throws NoResponseException if there was no response from the remote entity.
  561.      * @throws XMPPErrorException if there was an XMPP error returned.
  562.      * @throws NotConnectedException if the XMPP connection is not connected.
  563.      * @throws InterruptedException if the calling thread was interrupted.
  564.      * @throws NotAMucServiceException if the entity is not a MUC service.
  565.      */
  566.     public MucCreateConfigFormHandle createOrJoinIfNecessary(Resourcepart nickname, String password) throws NoResponseException,
  567.                     XMPPErrorException, NotConnectedException, InterruptedException, NotAMucServiceException {
  568.         if (isJoined()) {
  569.             return null;
  570.         }
  571.         MucEnterConfiguration mucEnterConfiguration = getEnterConfigurationBuilder(nickname).withPassword(
  572.                         password).build();
  573.         try {
  574.             return createOrJoin(mucEnterConfiguration);
  575.         }
  576.         catch (MucAlreadyJoinedException e) {
  577.             return null;
  578.         }
  579.     }

  580.     /**
  581.      * Joins the chat room using the specified nickname. If already joined
  582.      * using another nickname, this method will first leave the room and then
  583.      * re-join using the new nickname. The default connection timeout for a reply
  584.      * from the group chat server that the join succeeded will be used. After
  585.      * joining the room, the room will decide the amount of history to send.
  586.      *
  587.      * @param nickname the nickname to use.
  588.      * @return the leave self-presence as reflected by the MUC.
  589.      * @throws NoResponseException if there was no response from the remote entity.
  590.      * @throws XMPPErrorException if an error occurs joining the room. In particular, a
  591.      *      401 error can occur if no password was provided and one is required; or a
  592.      *      403 error can occur if the user is banned; or a
  593.      *      404 error can occur if the room does not exist or is locked; or a
  594.      *      407 error can occur if user is not on the member list; or a
  595.      *      409 error can occur if someone is already in the group chat with the same nickname.
  596.      * @throws NoResponseException if there was no response from the server.
  597.      * @throws NotConnectedException if the XMPP connection is not connected.
  598.      * @throws InterruptedException if the calling thread was interrupted.
  599.      * @throws NotAMucServiceException if the entity is not a MUC service.
  600.      */
  601.     public Presence join(Resourcepart nickname) throws NoResponseException, XMPPErrorException,
  602.                     NotConnectedException, InterruptedException, NotAMucServiceException {
  603.         MucEnterConfiguration.Builder builder = getEnterConfigurationBuilder(nickname);
  604.         Presence reflectedJoinPresence = join(builder.build());
  605.         return reflectedJoinPresence;
  606.     }

  607.     /**
  608.      * Joins the chat room using the specified nickname and password. If already joined
  609.      * using another nickname, this method will first leave the room and then
  610.      * re-join using the new nickname. The default connection timeout for a reply
  611.      * from the group chat server that the join succeeded will be used. After
  612.      * joining the room, the room will decide the amount of history to send.<p>
  613.      *
  614.      * A password is required when joining password protected rooms. If the room does
  615.      * not require a password there is no need to provide one.
  616.      *
  617.      * @param nickname the nickname to use.
  618.      * @param password the password to use.
  619.      * @throws XMPPErrorException if an error occurs joining the room. In particular, a
  620.      *      401 error can occur if no password was provided and one is required; or a
  621.      *      403 error can occur if the user is banned; or a
  622.      *      404 error can occur if the room does not exist or is locked; or a
  623.      *      407 error can occur if user is not on the member list; or a
  624.      *      409 error can occur if someone is already in the group chat with the same nickname.
  625.      * @throws InterruptedException if the calling thread was interrupted.
  626.      * @throws NotConnectedException if the XMPP connection is not connected.
  627.      * @throws NoResponseException if there was no response from the server.
  628.      * @throws NotAMucServiceException if the entity is not a MUC service.
  629.      */
  630.     public void join(Resourcepart nickname, String password) throws XMPPErrorException, InterruptedException, NoResponseException, NotConnectedException, NotAMucServiceException {
  631.         MucEnterConfiguration.Builder builder = getEnterConfigurationBuilder(nickname).withPassword(
  632.                         password);
  633.         join(builder.build());
  634.     }

  635.     /**
  636.      * Joins the chat room using the specified nickname and password. If already joined
  637.      * using another nickname, this method will first leave the room and then
  638.      * re-join using the new nickname.<p>
  639.      *
  640.      * To control the amount of history to receive while joining a room you will need to provide
  641.      * a configured DiscussionHistory object.<p>
  642.      *
  643.      * A password is required when joining password protected rooms. If the room does
  644.      * not require a password there is no need to provide one.<p>
  645.      *
  646.      * If the room does not already exist when the user seeks to enter it, the server will
  647.      * decide to create a new room or not.
  648.      *
  649.      * @param mucEnterConfiguration the configuration used to enter the MUC.
  650.      * @return the join self-presence as reflected by the MUC.
  651.      * @throws XMPPErrorException if an error occurs joining the room. In particular, a
  652.      *      401 error can occur if no password was provided and one is required; or a
  653.      *      403 error can occur if the user is banned; or a
  654.      *      404 error can occur if the room does not exist or is locked; or a
  655.      *      407 error can occur if user is not on the member list; or a
  656.      *      409 error can occur if someone is already in the group chat with the same nickname.
  657.      * @throws NoResponseException if there was no response from the server.
  658.      * @throws NotConnectedException if the XMPP connection is not connected.
  659.      * @throws InterruptedException if the calling thread was interrupted.
  660.      * @throws NotAMucServiceException if the entity is not a MUC service.
  661.      */
  662.     public synchronized Presence join(MucEnterConfiguration mucEnterConfiguration)
  663.         throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException, NotAMucServiceException {
  664.         // If we've already joined the room, leave it before joining under a new
  665.         // nickname.
  666.         if (isJoined()) {
  667.             try {
  668.                 leave();
  669.             }
  670.             catch (XMPPErrorException | NoResponseException | MucNotJoinedException e) {
  671.                 LOGGER.log(Level.WARNING, "Could not leave MUC prior joining, assuming we are not joined", e);
  672.             }
  673.         }
  674.         Presence reflectedJoinPresence = enter(mucEnterConfiguration);
  675.         return reflectedJoinPresence;
  676.     }

  677.     /**
  678.      * Returns true if currently in the multi user chat (after calling the {@link
  679.      * #join(Resourcepart)} method).
  680.      *
  681.      * @return true if currently in the multi user chat room.
  682.      */
  683.     public boolean isJoined() {
  684.         return getMyRoomJid() != null;
  685.     }

  686.     /**
  687.      * Leave the chat room.
  688.      *
  689.      * @return the leave presence as reflected by the MUC.
  690.      * @throws NotConnectedException if the XMPP connection is not connected.
  691.      * @throws InterruptedException if the calling thread was interrupted.
  692.      * @throws XMPPErrorException if there was an XMPP error returned.
  693.      * @throws NoResponseException if there was no response from the remote entity.
  694.      * @throws MucNotJoinedException if not joined to the Multi-User Chat.
  695.      */
  696.     public synchronized Presence leave()
  697.                     throws NotConnectedException, InterruptedException, NoResponseException, XMPPErrorException, MucNotJoinedException {
  698.         //  Note that this method is intentionally not guarded by
  699.         // "if  (!joined) return" because it should be always be possible to leave the room in case the instance's
  700.         // state does not reflect the actual state.

  701.         final EntityFullJid myRoomJid = getMyRoomJid();
  702.         if (myRoomJid == null) {
  703.             throw new MucNotJoinedException(this);
  704.         }

  705.         // TODO: Consider adding a origin-id to the presence, once it is moved form smack-experimental into
  706.         // smack-extensions, in case the MUC service does not support stable IDs, and modify
  707.         // reflectedLeavePresenceFilters accordingly.

  708.         // We leave a room by sending a presence packet where the "to"
  709.         // field is in the form "roomName@service/nickname"
  710.         Presence leavePresence = connection.getStanzaFactory().buildPresenceStanza()
  711.                 .ofType(Presence.Type.unavailable)
  712.                 .to(myRoomJid)
  713.                 .build();

  714.         List<StanzaFilter> reflectedLeavePresenceFilters = new ArrayList<>(3);
  715.         reflectedLeavePresenceFilters.add(StanzaTypeFilter.PRESENCE);
  716.         reflectedLeavePresenceFilters.add(new OrFilter(
  717.                         new AndFilter(FromMatchesFilter.createFull(myRoomJid), PresenceTypeFilter.UNAVAILABLE,
  718.                                         MUCUserStatusCodeFilter.STATUS_110_PRESENCE_TO_SELF),
  719.                         new AndFilter(fromRoomFilter, PresenceTypeFilter.ERROR)));

  720.         if (serviceSupportsStableIds()) {
  721.             reflectedLeavePresenceFilters.add(new StanzaIdFilter(leavePresence));
  722.         }

  723.         StanzaFilter reflectedLeavePresenceFilter = new AndFilter(reflectedLeavePresenceFilters);

  724.         Presence reflectedLeavePresence;
  725.         try {
  726.             reflectedLeavePresence = connection.createStanzaCollectorAndSend(reflectedLeavePresenceFilter, leavePresence).nextResultOrThrow();
  727.         } finally {
  728.             // Reset occupant information after we send the leave presence. This ensures that we only call userHasLeft()
  729.             // and reset the local MUC state after we successfully left the MUC (or if an exception occurred).
  730.             userHasLeft();
  731.         }

  732.         return reflectedLeavePresence;
  733.     }

  734.     /**
  735.      * Get a {@link MucConfigFormManager} to configure this room.
  736.      * <p>
  737.      * Only room owners are able to configure a room.
  738.      * </p>
  739.      *
  740.      * @return a MUC configuration form manager for this room.
  741.      * @throws NoResponseException if there was no response from the remote entity.
  742.      * @throws XMPPErrorException if there was an XMPP error returned.
  743.      * @throws NotConnectedException if the XMPP connection is not connected.
  744.      * @throws InterruptedException if the calling thread was interrupted.
  745.      * @see <a href="http://xmpp.org/extensions/xep-0045.html#roomconfig">XEP-45 § 10.2 Subsequent Room Configuration</a>
  746.      * @since 4.2
  747.      */
  748.     public MucConfigFormManager getConfigFormManager() throws NoResponseException,
  749.                     XMPPErrorException, NotConnectedException, InterruptedException {
  750.         return new MucConfigFormManager(this);
  751.     }

  752.     /**
  753.      * Returns the room's configuration form that the room's owner can use.
  754.      * The configuration form allows to set the room's language,
  755.      * enable logging, specify room's type, etc.
  756.      *
  757.      * @return the Form that contains the fields to complete together with the instructions or
  758.      * <code>null</code> if no configuration is possible.
  759.      * @throws XMPPErrorException if an error occurs asking the configuration form for the room.
  760.      * @throws NoResponseException if there was no response from the server.
  761.      * @throws NotConnectedException if the XMPP connection is not connected.
  762.      * @throws InterruptedException if the calling thread was interrupted.
  763.      */
  764.     public Form getConfigurationForm() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  765.         MUCOwner iq = new MUCOwner();
  766.         iq.setTo(room);
  767.         iq.setType(IQ.Type.get);

  768.         IQ answer = connection.sendIqRequestAndWaitForResponse(iq);
  769.         DataForm dataForm = DataForm.from(answer, MucConfigFormManager.FORM_TYPE);
  770.         return new Form(dataForm);
  771.     }

  772.     /**
  773.      * Sends the completed configuration form to the server. The room will be configured
  774.      * with the new settings defined in the form.
  775.      *
  776.      * @param form the form with the new settings.
  777.      * @throws XMPPErrorException if an error occurs setting the new rooms' configuration.
  778.      * @throws NoResponseException if there was no response from the server.
  779.      * @throws NotConnectedException if the XMPP connection is not connected.
  780.      * @throws InterruptedException if the calling thread was interrupted.
  781.      */
  782.     public void sendConfigurationForm(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  783.         final DataForm dataForm;
  784.         if (form != null) {
  785.             dataForm = form.getDataFormToSubmit();
  786.         } else {
  787.             // Instant room, cf. XEP-0045 § 10.1.2
  788.             dataForm = DataForm.builder().build();
  789.         }

  790.         MUCOwner iq = new MUCOwner();
  791.         iq.setTo(room);
  792.         iq.setType(IQ.Type.set);
  793.         iq.addExtension(dataForm);

  794.         connection.sendIqRequestAndWaitForResponse(iq);
  795.     }

  796.     /**
  797.      * Returns the room's registration form that an unaffiliated user, can use to become a member
  798.      * of the room or <code>null</code> if no registration is possible. Some rooms may restrict the
  799.      * privilege to register members and allow only room admins to add new members.<p>
  800.      *
  801.      * If the user requesting registration requirements is not allowed to register with the room
  802.      * (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
  803.      * error to the user (error code 405).
  804.      *
  805.      * @return the registration Form that contains the fields to complete together with the
  806.      * instructions or <code>null</code> if no registration is possible.
  807.      * @throws XMPPErrorException if an error occurs asking the registration form for the room or a
  808.      * 405 error if the user is not allowed to register with the room.
  809.      * @throws NoResponseException if there was no response from the server.
  810.      * @throws NotConnectedException if the XMPP connection is not connected.
  811.      * @throws InterruptedException if the calling thread was interrupted.
  812.      */
  813.     public Form getRegistrationForm() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  814.         Registration reg = new Registration();
  815.         reg.setType(IQ.Type.get);
  816.         reg.setTo(room);

  817.         IQ result = connection.sendIqRequestAndWaitForResponse(reg);
  818.         DataForm dataForm = DataForm.from(result);
  819.         return new Form(dataForm);
  820.     }

  821.     /**
  822.      * Sends the completed registration form to the server. After the user successfully submits
  823.      * the form, the room may queue the request for review by the room admins or may immediately
  824.      * add the user to the member list by changing the user's affiliation from "none" to "member.<p>
  825.      *
  826.      * If the desired room nickname is already reserved for that room, the room will return a
  827.      * "Conflict" error to the user (error code 409). If the room does not support registration,
  828.      * it will return a "Service Unavailable" error to the user (error code 503).
  829.      *
  830.      * @param form the completed registration form.
  831.      * @throws XMPPErrorException if an error occurs submitting the registration form. In particular, a
  832.      *      409 error can occur if the desired room nickname is already reserved for that room;
  833.      *      or a 503 error can occur if the room does not support registration.
  834.      * @throws NoResponseException if there was no response from the server.
  835.      * @throws NotConnectedException if the XMPP connection is not connected.
  836.      * @throws InterruptedException if the calling thread was interrupted.
  837.      */
  838.     public void sendRegistrationForm(FillableForm form) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  839.         Registration reg = new Registration();
  840.         reg.setType(IQ.Type.set);
  841.         reg.setTo(room);
  842.         reg.addExtension(form.getDataFormToSubmit());

  843.         connection.sendIqRequestAndWaitForResponse(reg);
  844.     }

  845.     /**
  846.      * Sends a request to destroy the room.
  847.      *
  848.      * @throws XMPPErrorException if an error occurs while trying to destroy the room.
  849.      *      An error can occur which will be wrapped by an XMPPException --
  850.      *      XMPP error code 403. The error code can be used to present more
  851.      *      appropriate error messages to end-users.
  852.      * @throws NoResponseException if there was no response from the server.
  853.      * @throws NotConnectedException if the XMPP connection is not connected.
  854.      * @throws InterruptedException if the calling thread was interrupted.
  855.      * @see #destroy(String, EntityBareJid)
  856.      * @since 4.5
  857.      */
  858.     public void destroy() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  859.         destroy(null, null);
  860.     }

  861.     /**
  862.      * Sends a request to the server to destroy the room. The sender of the request
  863.      * should be the room's owner. If the sender of the destroy request is not the room's owner
  864.      * then the server will answer a "Forbidden" error (403).
  865.      *
  866.      * @param reason an optional reason for the room destruction.
  867.      * @param alternateJID an optional JID of an alternate location.
  868.      * @throws XMPPErrorException if an error occurs while trying to destroy the room.
  869.      *      An error can occur which will be wrapped by an XMPPException --
  870.      *      XMPP error code 403. The error code can be used to present more
  871.      *      appropriate error messages to end-users.
  872.      * @throws NoResponseException if there was no response from the server.
  873.      * @throws NotConnectedException if the XMPP connection is not connected.
  874.      * @throws InterruptedException if the calling thread was interrupted.
  875.      */
  876.     public void destroy(String reason, EntityBareJid alternateJID) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  877.         destroy(reason, alternateJID, null);
  878.     }

  879.     /**
  880.      * Sends a request to the server to destroy the room. The sender of the request
  881.      * should be the room's owner. If the sender of the destroy request is not the room's owner
  882.      * then the server will answer a "Forbidden" error (403).
  883.      *
  884.      * @param reason an optional reason for the room destruction.
  885.      * @param alternateJID an optional JID of an alternate location.
  886.      * @param password an optional password for the alternate location
  887.      * @throws XMPPErrorException if an error occurs while trying to destroy the room.
  888.      *      An error can occur which will be wrapped by an XMPPException --
  889.      *      XMPP error code 403. The error code can be used to present more
  890.      *      appropriate error messages to end-users.
  891.      * @throws NoResponseException if there was no response from the server.
  892.      * @throws NotConnectedException if the XMPP connection is not connected.
  893.      * @throws InterruptedException if the calling thread was interrupted.
  894.      */
  895.     public void destroy(String reason, EntityBareJid alternateJID, String password) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  896.         MUCOwner iq = new MUCOwner();
  897.         iq.setTo(room);
  898.         iq.setType(IQ.Type.set);

  899.         // Create the reason for the room destruction
  900.         Destroy destroy = new Destroy(alternateJID, password, reason);
  901.         iq.setDestroy(destroy);

  902.         try {
  903.             connection.sendIqRequestAndWaitForResponse(iq);
  904.         }
  905.         catch (XMPPErrorException e) {
  906.             // Note that we do not call userHasLeft() here because an XMPPErrorException would usually indicate that the
  907.             // room was not destroyed and we therefore we also did not leave the room.
  908.             throw e;
  909.         }
  910.         catch (NoResponseException | NotConnectedException | InterruptedException e) {
  911.             // Reset occupant information.
  912.             userHasLeft();
  913.             throw e;
  914.         }

  915.         // Reset occupant information.
  916.         userHasLeft();
  917.     }

  918.     /**
  919.      * Invites another user to the room in which one is an occupant. The invitation
  920.      * will be sent to the room which in turn will forward the invitation to the invitee.<p>
  921.      *
  922.      * If the room is password-protected, the invitee will receive a password to use to join
  923.      * the room. If the room is members-only, the invitee may be added to the member list.
  924.      *
  925.      * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
  926.      * @param reason the reason why the user is being invited.
  927.      * @throws NotConnectedException if the XMPP connection is not connected.
  928.      * @throws InterruptedException if the calling thread was interrupted.
  929.      */
  930.     public void invite(EntityBareJid user, String reason) throws NotConnectedException, InterruptedException {
  931.         invite(connection.getStanzaFactory().buildMessageStanza(), user, reason);
  932.     }

  933.     /**
  934.      * Invites another user to the room in which one is an occupant using a given Message. The invitation
  935.      * will be sent to the room which in turn will forward the invitation to the invitee.<p>
  936.      *
  937.      * If the room is password-protected, the invitee will receive a password to use to join
  938.      * the room. If the room is members-only, the invitee may be added to the member list.
  939.      *
  940.      * @param messageBuilder the message to use for sending the invitation.
  941.      * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
  942.      * @param reason the reason why the user is being invited.
  943.      * @throws NotConnectedException if the XMPP connection is not connected.
  944.      * @throws InterruptedException if the calling thread was interrupted.
  945.      */
  946.     public void invite(MessageBuilder messageBuilder, EntityBareJid user, String reason) throws NotConnectedException, InterruptedException {
  947.         // TODO listen for 404 error code when inviter supplies a non-existent JID
  948.         messageBuilder.to(room);

  949.         // Create the MUCUser packet that will include the invitation
  950.         MUCUser mucUser = new MUCUser();
  951.         MUCUser.Invite invite = new MUCUser.Invite(reason, user);
  952.         mucUser.setInvite(invite);
  953.         // Add the MUCUser packet that includes the invitation to the message
  954.         messageBuilder.addExtension(mucUser);

  955.         Message message = messageBuilder.build();
  956.         connection.sendStanza(message);
  957.     }

  958.     /**
  959.      * Invites another user to the room in which one is an occupant. In contrast
  960.      * to the method "invite", the invitation is sent directly to the user rather
  961.      * than via the chat room.  This is useful when the user being invited is
  962.      * offline, as otherwise the invitation would be dropped.
  963.      *
  964.      * @param address the user to send the invitation to
  965.      * @throws NotConnectedException if the XMPP connection is not connected.
  966.      * @throws InterruptedException if the calling thread was interrupted.
  967.      */
  968.     public void inviteDirectly(EntityBareJid address) throws NotConnectedException, InterruptedException {
  969.         inviteDirectly(address, null, null, false, null);
  970.     }

  971.      /**
  972.      * Invites another user to the room in which one is an occupant. In contrast
  973.      * to the method "invite", the invitation is sent directly to the user rather
  974.      * than via the chat room.  This is useful when the user being invited is
  975.      * offline, as otherwise the invitation would be dropped.
  976.      *
  977.      * @param address the user to send the invitation to
  978.      * @param reason the purpose for the invitation
  979.      * @param password specifies a password needed for entry
  980.      * @param continueAsOneToOneChat specifies if the groupchat room continues a one-to-one chat having the designated thread
  981.      * @param thread the thread to continue
  982.      * @throws NotConnectedException if the XMPP connection is not connected.
  983.      * @throws InterruptedException if the calling thread was interrupted.
  984.      */
  985.     public void inviteDirectly(EntityBareJid address, String reason, String password, boolean continueAsOneToOneChat, String thread)
  986.         throws NotConnectedException, InterruptedException {
  987.         // Add the extension for direct invitation
  988.         GroupChatInvitation invitationExt = new GroupChatInvitation(room,
  989.                                                                     reason,
  990.                                                                     password,
  991.                                                                     continueAsOneToOneChat,
  992.                                                                     thread);

  993.         Message message = connection.getStanzaFactory().buildMessageStanza()
  994.                 .to(address)
  995.                 .addExtension(invitationExt)
  996.                 .build();

  997.         connection.sendStanza(message);
  998.     }

  999.     /**
  1000.      * Adds a listener to invitation rejections notifications. The listener will be fired anytime
  1001.      * an invitation is declined.
  1002.      *
  1003.      * @param listener an invitation rejection listener.
  1004.      * @return true if the listener was not already added.
  1005.      */
  1006.     public boolean addInvitationRejectionListener(InvitationRejectionListener listener) {
  1007.          return invitationRejectionListeners.add(listener);
  1008.     }

  1009.     /**
  1010.      * Removes a listener from invitation rejections notifications. The listener will be fired
  1011.      * anytime an invitation is declined.
  1012.      *
  1013.      * @param listener an invitation rejection listener.
  1014.      * @return true if the listener was registered and is now removed.
  1015.      */
  1016.     public boolean removeInvitationRejectionListener(InvitationRejectionListener listener) {
  1017.         return invitationRejectionListeners.remove(listener);
  1018.     }

  1019.     /**
  1020.      * Fires invitation rejection listeners.
  1021.      *
  1022.      * @param message the message.
  1023.      * @param rejection the information about the rejection.
  1024.      */
  1025.     private void fireInvitationRejectionListeners(Message message, MUCUser.Decline rejection) {
  1026.         EntityBareJid invitee = rejection.getFrom();
  1027.         String reason = rejection.getReason();
  1028.         InvitationRejectionListener[] listeners;
  1029.         synchronized (invitationRejectionListeners) {
  1030.             listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
  1031.             invitationRejectionListeners.toArray(listeners);
  1032.         }
  1033.         for (InvitationRejectionListener listener : listeners) {
  1034.             listener.invitationDeclined(invitee, reason, message, rejection);
  1035.         }
  1036.     }

  1037.     /**
  1038.      * Adds a listener to subject change notifications. The listener will be fired anytime
  1039.      * the room's subject changes.
  1040.      *
  1041.      * @param listener a subject updated listener.
  1042.      * @return true if the listener was not already added.
  1043.      */
  1044.     public boolean addSubjectUpdatedListener(SubjectUpdatedListener listener) {
  1045.         return subjectUpdatedListeners.add(listener);
  1046.     }

  1047.     /**
  1048.      * Removes a listener from subject change notifications. The listener will be fired
  1049.      * anytime the room's subject changes.
  1050.      *
  1051.      * @param listener a subject updated listener.
  1052.      * @return true if the listener was registered and is now removed.
  1053.      */
  1054.     public boolean removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
  1055.         return subjectUpdatedListeners.remove(listener);
  1056.     }

  1057.     /**
  1058.      * Adds a new {@link StanzaListener} that will be invoked every time a new presence
  1059.      * is going to be sent by this MultiUserChat to the server. Stanza interceptors may
  1060.      * add new extensions to the presence that is going to be sent to the MUC service.
  1061.      *
  1062.      * @param presenceInterceptor the new stanza interceptor that will intercept presence packets.
  1063.      */
  1064.     public void addPresenceInterceptor(Consumer<PresenceBuilder> presenceInterceptor) {
  1065.         boolean added = presenceInterceptors.add(presenceInterceptor);
  1066.         if (!added) return;
  1067.         int currentCount = presenceInterceptorCount.incrementAndGet();
  1068.         if (currentCount == 1) {
  1069.             connection.addPresenceInterceptor(this.presenceInterceptor, ToMatchesFilter.create(room).asPredicate(Presence.class));
  1070.         }
  1071.     }

  1072.     /**
  1073.      * Removes a {@link StanzaListener} that was being invoked every time a new presence
  1074.      * was being sent by this MultiUserChat to the server. Stanza interceptors may
  1075.      * add new extensions to the presence that is going to be sent to the MUC service.
  1076.      *
  1077.      * @param presenceInterceptor the stanza interceptor to remove.
  1078.      */
  1079.     public void removePresenceInterceptor(Consumer<PresenceBuilder> presenceInterceptor) {
  1080.         boolean removed = presenceInterceptors.remove(presenceInterceptor);
  1081.         if (!removed) return;
  1082.         int currentCount = presenceInterceptorCount.decrementAndGet();
  1083.         if (currentCount == 0) {
  1084.             connection.removePresenceInterceptor(this.presenceInterceptor);
  1085.         }
  1086.     }

  1087.     /**
  1088.      * Returns the last known room's subject or <code>null</code> if the user hasn't joined the room
  1089.      * or the room does not have a subject yet. In case the room has a subject, as soon as the
  1090.      * user joins the room a message with the current room's subject will be received.<p>
  1091.      *
  1092.      * To be notified every time the room's subject change you should add a listener
  1093.      * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
  1094.      *
  1095.      * To change the room's subject use {@link #changeSubject(String)}.
  1096.      *
  1097.      * @return the room's subject or <code>null</code> if the user hasn't joined the room or the
  1098.      * room does not have a subject yet.
  1099.      */
  1100.     public String getSubject() {
  1101.         return subject;
  1102.     }

  1103.     /**
  1104.      * Returns the reserved room nickname for the user in the room. A user may have a reserved
  1105.      * nickname, for example through explicit room registration or database integration. In such
  1106.      * cases it may be desirable for the user to discover the reserved nickname before attempting
  1107.      * to enter the room.
  1108.      *
  1109.      * @return the reserved room nickname or <code>null</code> if none.
  1110.      * @throws SmackException if there was no response from the server.
  1111.      * @throws InterruptedException if the calling thread was interrupted.
  1112.      */
  1113.     public String getReservedNickname() throws SmackException, InterruptedException {
  1114.         try {
  1115.             DiscoverInfo result =
  1116.                 ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
  1117.                     room,
  1118.                     "x-roomuser-item");
  1119.             // Look for an Identity that holds the reserved nickname and return its name
  1120.             for (DiscoverInfo.Identity identity : result.getIdentities()) {
  1121.                 return identity.getName();
  1122.             }
  1123.         }
  1124.         catch (XMPPException e) {
  1125.             LOGGER.log(Level.SEVERE, "Error retrieving room nickname", e);
  1126.         }
  1127.         // If no Identity was found then the user does not have a reserved room nickname
  1128.         return null;
  1129.     }

  1130.     /**
  1131.      * Returns the nickname that was used to join the room, or <code>null</code> if not
  1132.      * currently joined.
  1133.      *
  1134.      * @return the nickname currently being used.
  1135.      */
  1136.     public Resourcepart getNickname() {
  1137.         final EntityFullJid myRoomJid = getMyRoomJid();
  1138.         if (myRoomJid == null) {
  1139.             return null;
  1140.         }
  1141.         return myRoomJid.getResourcepart();
  1142.     }

  1143.     /**
  1144.      * Return the full JID of the user in the room, or <code>null</code> if the room is not joined.
  1145.      *
  1146.      * @return the full JID of the user in the room, or <code>null</code>.
  1147.      * @since 4.5.0
  1148.      */
  1149.     public EntityFullJid getMyRoomJid() {
  1150.         return myRoomJid;
  1151.     }

  1152.     private static final Object changeNicknameLock = new Object();

  1153.     /**
  1154.      * Changes the occupant's nickname to a new nickname within the room. Each room occupant
  1155.      * will receive two presence packets. One of type "unavailable" for the old nickname and one
  1156.      * indicating availability for the new nickname. The unavailable presence will contain the new
  1157.      * nickname and an appropriate status code (namely 303) as extended presence information. The
  1158.      * status code 303 indicates that the occupant is changing his/her nickname.
  1159.      *
  1160.      * @param nickname the new nickname within the room.
  1161.      * @throws XMPPErrorException if the new nickname is already in use by another occupant.
  1162.      * @throws NoResponseException if there was no response from the server.
  1163.      * @throws NotConnectedException if the XMPP connection is not connected.
  1164.      * @throws InterruptedException if the calling thread was interrupted.
  1165.      * @throws MucNotJoinedException if not joined to the Multi-User Chat.
  1166.      */
  1167.     public void changeNickname(Resourcepart nickname) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException, MucNotJoinedException  {
  1168.         Objects.requireNonNull(nickname, "Nickname must not be null or blank.");
  1169.         // Check that we already have joined the room before attempting to change the
  1170.         // nickname.
  1171.         if (!isJoined()) {
  1172.             throw new MucNotJoinedException(this);
  1173.         }
  1174.         final EntityFullJid jid = JidCreate.entityFullFrom(room, nickname);
  1175.         // We change the nickname by sending a presence packet where the "to"
  1176.         // field is in the form "roomName@service/nickname"
  1177.         // We don't have to signal the MUC support again
  1178.         Presence joinPresence = connection.getStanzaFactory().buildPresenceStanza()
  1179.                 .to(jid)
  1180.                 .ofType(Presence.Type.available)
  1181.                 .build();

  1182.         synchronized (changeNicknameLock) {
  1183.             // Wait for a presence packet back from the server.
  1184.             StanzaFilter responseFilter =
  1185.                 new AndFilter(
  1186.                    FromMatchesFilter.createFull(jid),
  1187.                    new StanzaTypeFilter(Presence.class));
  1188.             StanzaCollector response = connection.createStanzaCollectorAndSend(responseFilter, joinPresence);
  1189.             // Wait up to a certain number of seconds for a reply. If there is a negative reply, an
  1190.             // exception will be thrown
  1191.             response.nextResultOrThrow();

  1192.             // TODO: Shouldn't this handle nickname rewriting by the MUC service?
  1193.             setNickname(nickname);
  1194.         }
  1195.     }

  1196.     /**
  1197.      * Changes the occupant's availability status within the room. The presence type
  1198.      * will remain available but with a new status that describes the presence update and
  1199.      * a new presence mode (e.g. Extended away).
  1200.      *
  1201.      * @param status a text message describing the presence update.
  1202.      * @param mode the mode type for the presence update.
  1203.      * @throws NotConnectedException if the XMPP connection is not connected.
  1204.      * @throws InterruptedException if the calling thread was interrupted.
  1205.      * @throws MucNotJoinedException if not joined to the Multi-User Chat.
  1206.      */
  1207.     public void changeAvailabilityStatus(String status, Presence.Mode mode) throws NotConnectedException, InterruptedException, MucNotJoinedException {
  1208.         final EntityFullJid myRoomJid = getMyRoomJid();
  1209.         if (myRoomJid == null) {
  1210.             throw new MucNotJoinedException(this);
  1211.         }

  1212.         // We change the availability status by sending a presence packet to the room with the
  1213.         // new presence status and mode
  1214.         Presence joinPresence = connection.getStanzaFactory().buildPresenceStanza()
  1215.                 .to(myRoomJid)
  1216.                 .ofType(Presence.Type.available)
  1217.                 .setStatus(status)
  1218.                 .setMode(mode)
  1219.                 .build();

  1220.         // Send join packet.
  1221.         connection.sendStanza(joinPresence);
  1222.     }

  1223.     /**
  1224.      * Kicks a visitor or participant from the room. The kicked occupant will receive a presence
  1225.      * of type "unavailable" including a status code 307 and optionally along with the reason
  1226.      * (if provided) and the bare JID of the user who initiated the kick. After the occupant
  1227.      * was kicked from the room, the rest of the occupants will receive a presence of type
  1228.      * "unavailable". The presence will include a status code 307 which means that the occupant
  1229.      * was kicked from the room.
  1230.      *
  1231.      * @param nickname the nickname of the participant or visitor to kick from the room
  1232.      * (e.g. "john").
  1233.      * @param reason the reason why the participant or visitor is being kicked from the room.
  1234.      * @throws XMPPErrorException if an error occurs kicking the occupant. In particular, a
  1235.      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
  1236.      *      was intended to be kicked (i.e. Not Allowed error); or a
  1237.      *      403 error can occur if the occupant that intended to kick another occupant does
  1238.      *      not have kicking privileges (i.e. Forbidden error); or a
  1239.      *      400 error can occur if the provided nickname is not present in the room.
  1240.      * @throws NoResponseException if there was no response from the server.
  1241.      * @throws NotConnectedException if the XMPP connection is not connected.
  1242.      * @throws InterruptedException if the calling thread was interrupted.
  1243.      */
  1244.     public void kickParticipant(Resourcepart nickname, String reason) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1245.         changeRole(nickname, MUCRole.none, reason);
  1246.     }

  1247.     /**
  1248.      * Sends a voice request to the MUC. The room moderators usually need to approve this request.
  1249.      *
  1250.      * @throws NotConnectedException if the XMPP connection is not connected.
  1251.      * @throws InterruptedException if the calling thread was interrupted.
  1252.      * @see <a href="http://xmpp.org/extensions/xep-0045.html#requestvoice">XEP-45 § 7.13 Requesting
  1253.      *      Voice</a>
  1254.      * @since 4.1
  1255.      */
  1256.     public void requestVoice() throws NotConnectedException, InterruptedException {
  1257.         DataForm.Builder form = DataForm.builder()
  1258.                         .setFormType(MUCInitialPresence.NAMESPACE + "#request");

  1259.         TextSingleFormField.Builder requestVoiceField = FormField.textSingleBuilder("muc#role");
  1260.         requestVoiceField.setLabel("Requested role");
  1261.         requestVoiceField.setValue("participant");
  1262.         form.addField(requestVoiceField.build());

  1263.         Message message = connection.getStanzaFactory().buildMessageStanza()
  1264.                 .to(room)
  1265.                 .addExtension(form.build())
  1266.                 .build();
  1267.         connection.sendStanza(message);
  1268.     }

  1269.     /**
  1270.      * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
  1271.      * who does and does not have "voice" in the room. To have voice means that a room occupant
  1272.      * is able to send messages to the room occupants.
  1273.      *
  1274.      * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
  1275.      * @throws XMPPErrorException if an error occurs granting voice to a visitor. In particular, a
  1276.      *      403 error can occur if the occupant that intended to grant voice is not
  1277.      *      a moderator in this room (i.e. Forbidden error); or a
  1278.      *      400 error can occur if the provided nickname is not present in the room.
  1279.      * @throws NoResponseException if there was no response from the server.
  1280.      * @throws NotConnectedException if the XMPP connection is not connected.
  1281.      * @throws InterruptedException if the calling thread was interrupted.
  1282.      */
  1283.     public void grantVoice(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1284.         changeRole(nicknames, MUCRole.participant);
  1285.     }

  1286.     /**
  1287.      * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
  1288.      * who does and does not have "voice" in the room. To have voice means that a room occupant
  1289.      * is able to send messages to the room occupants.
  1290.      *
  1291.      * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
  1292.      * @throws XMPPErrorException if an error occurs granting voice to a visitor. In particular, a
  1293.      *      403 error can occur if the occupant that intended to grant voice is not
  1294.      *      a moderator in this room (i.e. Forbidden error); or a
  1295.      *      400 error can occur if the provided nickname is not present in the room.
  1296.      * @throws NoResponseException if there was no response from the server.
  1297.      * @throws NotConnectedException if the XMPP connection is not connected.
  1298.      * @throws InterruptedException if the calling thread was interrupted.
  1299.      */
  1300.     public void grantVoice(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1301.         changeRole(nickname, MUCRole.participant, null);
  1302.     }

  1303.     /**
  1304.      * Revokes voice from participants in the room. In a moderated room, a moderator may want to
  1305.      * revoke an occupant's privileges to speak. To have voice means that a room occupant
  1306.      * is able to send messages to the room occupants.
  1307.      *
  1308.      * @param nicknames the nicknames of the participants to revoke voice (e.g. "john").
  1309.      * @throws XMPPErrorException if an error occurs revoking voice from a participant. In particular, a
  1310.      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
  1311.      *      was tried to revoke his voice (i.e. Not Allowed error); or a
  1312.      *      400 error can occur if the provided nickname is not present in the room.
  1313.      * @throws NoResponseException if there was no response from the server.
  1314.      * @throws NotConnectedException if the XMPP connection is not connected.
  1315.      * @throws InterruptedException if the calling thread was interrupted.
  1316.      */
  1317.     public void revokeVoice(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1318.         changeRole(nicknames, MUCRole.visitor);
  1319.     }

  1320.     /**
  1321.      * Revokes voice from a participant in the room. In a moderated room, a moderator may want to
  1322.      * revoke an occupant's privileges to speak. To have voice means that a room occupant
  1323.      * is able to send messages to the room occupants.
  1324.      *
  1325.      * @param nickname the nickname of the participant to revoke voice (e.g. "john").
  1326.      * @throws XMPPErrorException if an error occurs revoking voice from a participant. In particular, a
  1327.      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
  1328.      *      was tried to revoke his voice (i.e. Not Allowed error); or a
  1329.      *      400 error can occur if the provided nickname is not present in the room.
  1330.      * @throws NoResponseException if there was no response from the server.
  1331.      * @throws NotConnectedException if the XMPP connection is not connected.
  1332.      * @throws InterruptedException if the calling thread was interrupted.
  1333.      */
  1334.     public void revokeVoice(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1335.         changeRole(nickname, MUCRole.visitor, null);
  1336.     }

  1337.     /**
  1338.      * Bans users from the room. An admin or owner of the room can ban users from a room. This
  1339.      * means that the banned user will no longer be able to join the room unless the ban has been
  1340.      * removed. If the banned user was present in the room then he/she will be removed from the
  1341.      * room and notified that he/she was banned along with the reason (if provided) and the bare
  1342.      * XMPP user ID of the user who initiated the ban.
  1343.      *
  1344.      * @param jids the bare XMPP user IDs of the users to ban.
  1345.      * @throws XMPPErrorException if an error occurs banning a user. In particular, a
  1346.      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
  1347.      *      was tried to be banned (i.e. Not Allowed error).
  1348.      * @throws NoResponseException if there was no response from the server.
  1349.      * @throws NotConnectedException if the XMPP connection is not connected.
  1350.      * @throws InterruptedException if the calling thread was interrupted.
  1351.      */
  1352.     public void banUsers(Collection<? extends BareJid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1353.         changeAffiliationByAdmin(jids, MUCAffiliation.outcast);
  1354.     }

  1355.     /**
  1356.      * Bans a user from the room. An admin or owner of the room can ban users from a room. This
  1357.      * means that the banned user will no longer be able to join the room unless the ban has been
  1358.      * removed. If the banned user was present in the room then he/she will be removed from the
  1359.      * room and notified that he/she was banned along with the reason (if provided) and the bare
  1360.      * XMPP user ID of the user who initiated the ban.
  1361.      *
  1362.      * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org").
  1363.      * @param reason the optional reason why the user was banned.
  1364.      * @throws XMPPErrorException if an error occurs banning a user. In particular, a
  1365.      *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
  1366.      *      was tried to be banned (i.e. Not Allowed error).
  1367.      * @throws NoResponseException if there was no response from the server.
  1368.      * @throws NotConnectedException if the XMPP connection is not connected.
  1369.      * @throws InterruptedException if the calling thread was interrupted.
  1370.      */
  1371.     public void banUser(BareJid jid, String reason) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1372.         changeAffiliationByAdmin(jid, MUCAffiliation.outcast, reason);
  1373.     }

  1374.     /**
  1375.      * Grants membership to other users. Only administrators are able to grant membership. A user
  1376.      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
  1377.      * that a user cannot enter without being on the member list).
  1378.      *
  1379.      * @param jids the XMPP user IDs of the users to grant membership.
  1380.      * @throws XMPPErrorException if an error occurs granting membership to a user.
  1381.      * @throws NoResponseException if there was no response from the server.
  1382.      * @throws NotConnectedException if the XMPP connection is not connected.
  1383.      * @throws InterruptedException if the calling thread was interrupted.
  1384.      */
  1385.     public void grantMembership(Collection<? extends Jid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1386.         changeAffiliationByAdmin(jids, MUCAffiliation.member);
  1387.     }

  1388.     /**
  1389.      * Grants membership to a user. Only administrators are able to grant membership. A user
  1390.      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
  1391.      * that a user cannot enter without being on the member list).
  1392.      *
  1393.      * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org").
  1394.      * @throws XMPPErrorException if an error occurs granting membership to a user.
  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 grantMembership(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1400.         changeAffiliationByAdmin(jid, MUCAffiliation.member, null);
  1401.     }

  1402.     /**
  1403.      * Revokes users' membership. Only administrators are able to revoke membership. A user
  1404.      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
  1405.      * that a user cannot enter without being on the member list). If the user is in the room and
  1406.      * the room is of type members-only then the user will be removed from the room.
  1407.      *
  1408.      * @param jids the bare XMPP user IDs of the users to revoke membership.
  1409.      * @throws XMPPErrorException if an error occurs revoking membership to a user.
  1410.      * @throws NoResponseException if there was no response from the server.
  1411.      * @throws NotConnectedException if the XMPP connection is not connected.
  1412.      * @throws InterruptedException if the calling thread was interrupted.
  1413.      */
  1414.     public void revokeMembership(Collection<? extends BareJid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1415.         changeAffiliationByAdmin(jids, MUCAffiliation.none);
  1416.     }

  1417.     /**
  1418.      * Revokes a user's membership. Only administrators are able to revoke membership. A user
  1419.      * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
  1420.      * that a user cannot enter without being on the member list). If the user is in the room and
  1421.      * the room is of type members-only then the user will be removed from the room.
  1422.      *
  1423.      * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org").
  1424.      * @throws XMPPErrorException if an error occurs revoking membership to a user.
  1425.      * @throws NoResponseException if there was no response from the server.
  1426.      * @throws NotConnectedException if the XMPP connection is not connected.
  1427.      * @throws InterruptedException if the calling thread was interrupted.
  1428.      */
  1429.     public void revokeMembership(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1430.         changeAffiliationByAdmin(jid, MUCAffiliation.none, null);
  1431.     }

  1432.     /**
  1433.      * Grants moderator privileges to participants or visitors. Room administrators may grant
  1434.      * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
  1435.      * other users, modify room's subject plus all the participant privileges.
  1436.      *
  1437.      * @param nicknames the nicknames of the occupants to grant moderator privileges.
  1438.      * @throws XMPPErrorException if an error occurs granting moderator privileges to a user.
  1439.      * @throws NoResponseException if there was no response from the server.
  1440.      * @throws NotConnectedException if the XMPP connection is not connected.
  1441.      * @throws InterruptedException if the calling thread was interrupted.
  1442.      */
  1443.     public void grantModerator(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1444.         changeRole(nicknames, MUCRole.moderator);
  1445.     }

  1446.     /**
  1447.      * Grants moderator privileges to a participant or visitor. Room administrators may grant
  1448.      * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
  1449.      * other users, modify room's subject plus all the participant privileges.
  1450.      *
  1451.      * @param nickname the nickname of the occupant to grant moderator privileges.
  1452.      * @throws XMPPErrorException if an error occurs granting moderator privileges to a user.
  1453.      * @throws NoResponseException if there was no response from the server.
  1454.      * @throws NotConnectedException if the XMPP connection is not connected.
  1455.      * @throws InterruptedException if the calling thread was interrupted.
  1456.      */
  1457.     public void grantModerator(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1458.         changeRole(nickname, MUCRole.moderator, null);
  1459.     }

  1460.     /**
  1461.      * Revokes moderator privileges from other users. The occupant that loses moderator
  1462.      * privileges will become a participant. Room administrators may revoke moderator privileges
  1463.      * only to occupants whose affiliation is member or none. This means that an administrator is
  1464.      * not allowed to revoke moderator privileges from other room administrators or owners.
  1465.      *
  1466.      * @param nicknames the nicknames of the occupants to revoke moderator privileges.
  1467.      * @throws XMPPErrorException if an error occurs revoking moderator privileges from a user.
  1468.      * @throws NoResponseException if there was no response from the server.
  1469.      * @throws NotConnectedException if the XMPP connection is not connected.
  1470.      * @throws InterruptedException if the calling thread was interrupted.
  1471.      */
  1472.     public void revokeModerator(Collection<Resourcepart> nicknames) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1473.         changeRole(nicknames, MUCRole.participant);
  1474.     }

  1475.     /**
  1476.      * Revokes moderator privileges from another user. The occupant that loses moderator
  1477.      * privileges will become a participant. Room administrators may revoke moderator privileges
  1478.      * only to occupants whose affiliation is member or none. This means that an administrator is
  1479.      * not allowed to revoke moderator privileges from other room administrators or owners.
  1480.      *
  1481.      * @param nickname the nickname of the occupant to revoke moderator privileges.
  1482.      * @throws XMPPErrorException if an error occurs revoking moderator privileges from a user.
  1483.      * @throws NoResponseException if there was no response from the server.
  1484.      * @throws NotConnectedException if the XMPP connection is not connected.
  1485.      * @throws InterruptedException if the calling thread was interrupted.
  1486.      */
  1487.     public void revokeModerator(Resourcepart nickname) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1488.         changeRole(nickname, MUCRole.participant, null);
  1489.     }

  1490.     /**
  1491.      * Grants ownership privileges to other users. Room owners may grant ownership privileges.
  1492.      * Some room implementations will not allow to grant ownership privileges to other users.
  1493.      * An owner is allowed to change defining room features as well as perform all administrative
  1494.      * functions.
  1495.      *
  1496.      * @param jids the collection of bare XMPP user IDs of the users to grant ownership.
  1497.      * @throws XMPPErrorException if an error occurs granting ownership privileges to a user.
  1498.      * @throws NoResponseException if there was no response from the server.
  1499.      * @throws NotConnectedException if the XMPP connection is not connected.
  1500.      * @throws InterruptedException if the calling thread was interrupted.
  1501.      */
  1502.     public void grantOwnership(Collection<? extends BareJid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1503.         changeAffiliationByAdmin(jids, MUCAffiliation.owner);
  1504.     }

  1505.     /**
  1506.      * Grants ownership privileges to another user. Room owners may grant ownership privileges.
  1507.      * Some room implementations will not allow to grant ownership privileges to other users.
  1508.      * An owner is allowed to change defining room features as well as perform all administrative
  1509.      * functions.
  1510.      *
  1511.      * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org").
  1512.      * @throws XMPPErrorException if an error occurs granting ownership privileges to a user.
  1513.      * @throws NoResponseException if there was no response from the server.
  1514.      * @throws NotConnectedException if the XMPP connection is not connected.
  1515.      * @throws InterruptedException if the calling thread was interrupted.
  1516.      */
  1517.     public void grantOwnership(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1518.         changeAffiliationByAdmin(jid, MUCAffiliation.owner, null);
  1519.     }

  1520.     /**
  1521.      * Revokes ownership privileges from other users. The occupant that loses ownership
  1522.      * privileges will become an administrator. Room owners may revoke ownership privileges.
  1523.      * Some room implementations will not allow to grant ownership privileges to other users.
  1524.      *
  1525.      * @param jids the bare XMPP user IDs of the users to revoke ownership.
  1526.      * @throws XMPPErrorException if an error occurs revoking ownership privileges from a user.
  1527.      * @throws NoResponseException if there was no response from the server.
  1528.      * @throws NotConnectedException if the XMPP connection is not connected.
  1529.      * @throws InterruptedException if the calling thread was interrupted.
  1530.      */
  1531.     public void revokeOwnership(Collection<? extends BareJid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1532.         changeAffiliationByAdmin(jids, MUCAffiliation.admin);
  1533.     }

  1534.     /**
  1535.      * Revokes ownership privileges from another user. The occupant that loses ownership
  1536.      * privileges will become an administrator. Room owners may revoke ownership privileges.
  1537.      * Some room implementations will not allow to grant ownership privileges to other users.
  1538.      *
  1539.      * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org").
  1540.      * @throws XMPPErrorException if an error occurs revoking ownership privileges from a user.
  1541.      * @throws NoResponseException if there was no response from the server.
  1542.      * @throws NotConnectedException if the XMPP connection is not connected.
  1543.      * @throws InterruptedException if the calling thread was interrupted.
  1544.      */
  1545.     public void revokeOwnership(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1546.         changeAffiliationByAdmin(jid, MUCAffiliation.admin, null);
  1547.     }

  1548.     /**
  1549.      * Grants administrator privileges to other users. Room owners may grant administrator
  1550.      * privileges to a member or unaffiliated user. An administrator is allowed to perform
  1551.      * administrative functions such as banning users and edit moderator list.
  1552.      *
  1553.      * @param jids the bare XMPP user IDs of the users to grant administrator privileges.
  1554.      * @throws XMPPErrorException if an error occurs granting administrator privileges to a user.
  1555.      * @throws NoResponseException if there was no response from the server.
  1556.      * @throws NotConnectedException if the XMPP connection is not connected.
  1557.      * @throws InterruptedException if the calling thread was interrupted.
  1558.      */
  1559.     public void grantAdmin(Collection<? extends BareJid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1560.         changeAffiliationByAdmin(jids, MUCAffiliation.admin);
  1561.     }

  1562.     /**
  1563.      * Grants administrator privileges to another user. Room owners may grant administrator
  1564.      * privileges to a member or unaffiliated user. An administrator is allowed to perform
  1565.      * administrative functions such as banning users and edit moderator list.
  1566.      *
  1567.      * @param jid the bare XMPP user ID of the user to grant administrator privileges
  1568.      * (e.g. "user@host.org").
  1569.      * @throws XMPPErrorException if an error occurs granting administrator privileges to a user.
  1570.      * @throws NoResponseException if there was no response from the server.
  1571.      * @throws NotConnectedException if the XMPP connection is not connected.
  1572.      * @throws InterruptedException if the calling thread was interrupted.
  1573.      */
  1574.     public void grantAdmin(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1575.         changeAffiliationByAdmin(jid, MUCAffiliation.admin);
  1576.     }

  1577.     /**
  1578.      * Revokes administrator privileges from users. The occupant that loses administrator
  1579.      * privileges will become a member. Room owners may revoke administrator privileges from
  1580.      * a member or unaffiliated user.
  1581.      *
  1582.      * @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
  1583.      * @throws XMPPErrorException if an error occurs revoking administrator privileges from a user.
  1584.      * @throws NoResponseException if there was no response from the server.
  1585.      * @throws NotConnectedException if the XMPP connection is not connected.
  1586.      * @throws InterruptedException if the calling thread was interrupted.
  1587.      */
  1588.     public void revokeAdmin(Collection<? extends BareJid> jids) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1589.         changeAffiliationByAdmin(jids, MUCAffiliation.admin);
  1590.     }

  1591.     /**
  1592.      * Revokes administrator privileges from a user. The occupant that loses administrator
  1593.      * privileges will become a member. Room owners may revoke administrator privileges from
  1594.      * a member or unaffiliated user.
  1595.      *
  1596.      * @param jid the bare XMPP user ID of the user to revoke administrator privileges
  1597.      * (e.g. "user@host.org").
  1598.      * @throws XMPPErrorException if an error occurs revoking administrator privileges from a user.
  1599.      * @throws NoResponseException if there was no response from the server.
  1600.      * @throws NotConnectedException if the XMPP connection is not connected.
  1601.      * @throws InterruptedException if the calling thread was interrupted.
  1602.      */
  1603.     public void revokeAdmin(BareJid jid) throws XMPPErrorException, NoResponseException, NotConnectedException, InterruptedException {
  1604.         changeAffiliationByAdmin(jid, MUCAffiliation.member);
  1605.     }

  1606.     /**
  1607.      * Tries to change the affiliation with an 'muc#admin' namespace
  1608.      *
  1609.      * @param jid TODO javadoc me please
  1610.      * @param affiliation TODO javadoc me please
  1611.      * @throws XMPPErrorException if there was an XMPP error returned.
  1612.      * @throws NoResponseException if there was no response from the remote entity.
  1613.      * @throws NotConnectedException if the XMPP connection is not connected.
  1614.      * @throws InterruptedException if the calling thread was interrupted.
  1615.      */
  1616.     private void changeAffiliationByAdmin(Jid jid, MUCAffiliation affiliation)
  1617.                     throws NoResponseException, XMPPErrorException,
  1618.                     NotConnectedException, InterruptedException {
  1619.         changeAffiliationByAdmin(jid, affiliation, null);
  1620.     }

  1621.     /**
  1622.      * Tries to change the affiliation with an 'muc#admin' namespace
  1623.      *
  1624.      * @param jid TODO javadoc me please
  1625.      * @param affiliation TODO javadoc me please
  1626.      * @param reason the reason for the affiliation change (optional)
  1627.      * @throws XMPPErrorException if there was an XMPP error returned.
  1628.      * @throws NoResponseException if there was no response from the remote entity.
  1629.      * @throws NotConnectedException if the XMPP connection is not connected.
  1630.      * @throws InterruptedException if the calling thread was interrupted.
  1631.      */
  1632.     private void changeAffiliationByAdmin(Jid jid, MUCAffiliation affiliation, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  1633.         MUCAdmin iq = new MUCAdmin();
  1634.         iq.setTo(room);
  1635.         iq.setType(IQ.Type.set);
  1636.         // Set the new affiliation.
  1637.         MUCItem item = new MUCItem(affiliation, jid, reason);
  1638.         iq.addItem(item);

  1639.         connection.sendIqRequestAndWaitForResponse(iq);
  1640.     }

  1641.     private void changeAffiliationByAdmin(Collection<? extends Jid> jids, MUCAffiliation affiliation)
  1642.                     throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  1643.         MUCAdmin iq = new MUCAdmin();
  1644.         iq.setTo(room);
  1645.         iq.setType(IQ.Type.set);
  1646.         for (Jid jid : jids) {
  1647.             // Set the new affiliation.
  1648.             MUCItem item = new MUCItem(affiliation, jid);
  1649.             iq.addItem(item);
  1650.         }

  1651.         connection.sendIqRequestAndWaitForResponse(iq);
  1652.     }

  1653.     private void changeRole(Resourcepart nickname, MUCRole role, String reason) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  1654.         MUCAdmin iq = new MUCAdmin();
  1655.         iq.setTo(room);
  1656.         iq.setType(IQ.Type.set);
  1657.         // Set the new role.
  1658.         MUCItem item = new MUCItem(role, nickname, reason);
  1659.         iq.addItem(item);

  1660.         connection.sendIqRequestAndWaitForResponse(iq);
  1661.     }

  1662.     private void changeRole(Collection<Resourcepart> nicknames, MUCRole role) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
  1663.         MUCAdmin iq = new MUCAdmin();
  1664.         iq.setTo(room);
  1665.         iq.setType(IQ.Type.set);
  1666.         for (Resourcepart nickname : nicknames) {
  1667.             // Set the new role.
  1668.             MUCItem item = new MUCItem(role, nickname);
  1669.             iq.addItem(item);
  1670.         }

  1671.         connection.sendIqRequestAndWaitForResponse(iq);
  1672.     }

  1673.     /**
  1674.      * Returns the number of occupants in the group chat.<p>
  1675.      *
  1676.      * Note: this value will only be accurate after joining the group chat, and
  1677.      * may fluctuate over time. If you query this value directly after joining the
  1678.      * group chat it may not be accurate, as it takes a certain amount of time for
  1679.      * the server to send all presence packets to this client.
  1680.      *
  1681.      * @return the number of occupants in the group chat.
  1682.      */
  1683.     public int getOccupantsCount() {
  1684.         return occupantsMap.size();
  1685.     }

  1686.     /**
  1687.      * Returns an List  for the list of fully qualified occupants
  1688.      * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
  1689.      * Typically, a client would only display the nickname of the occupant. To
  1690.      * get the nickname from the fully qualified name, use the
  1691.      * {@link org.jxmpp.util.XmppStringUtils#parseResource(String)} method.
  1692.      * Note: this value will only be accurate after joining the group chat, and may
  1693.      * fluctuate over time.
  1694.      *
  1695.      * @return a List of the occupants in the group chat.
  1696.      */
  1697.     public List<EntityFullJid> getOccupants() {
  1698.         return new ArrayList<>(occupantsMap.keySet());
  1699.     }

  1700.     /**
  1701.      * Returns the presence info for a particular user, or <code>null</code> if the user
  1702.      * is not in the room.<p>
  1703.      *
  1704.      * @param user the room occupant to search for his presence. The format of user must
  1705.      * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
  1706.      * @return the occupant's current presence, or <code>null</code> if the user is unavailable
  1707.      *      or if no presence information is available.
  1708.      */
  1709.     public Presence getOccupantPresence(EntityFullJid user) {
  1710.         return occupantsMap.get(user);
  1711.     }

  1712.     /**
  1713.      * Returns the Occupant information for a particular occupant, or <code>null</code> if the
  1714.      * user is not in the room. The Occupant object may include information such as full
  1715.      * JID of the user as well as the role and affiliation of the user in the room.<p>
  1716.      *
  1717.      * @param user the room occupant to search for his presence. The format of user must
  1718.      * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
  1719.      * @return the Occupant or <code>null</code> if the user is unavailable (i.e. not in the room).
  1720.      */
  1721.     public Occupant getOccupant(EntityFullJid user) {
  1722.         Presence presence = getOccupantPresence(user);
  1723.         if (presence != null) {
  1724.             return new Occupant(presence);
  1725.         }
  1726.         return null;
  1727.     }

  1728.     /**
  1729.      * Adds a stanza listener that will be notified of any new Presence packets
  1730.      * sent to the group chat. Using a listener is a suitable way to know when the list
  1731.      * of occupants should be re-loaded due to any changes.
  1732.      *
  1733.      * @param listener a stanza listener that will be notified of any presence packets
  1734.      *      sent to the group chat.
  1735.      * @return true if the listener was not already added.
  1736.      */
  1737.     public boolean addParticipantListener(PresenceListener listener) {
  1738.         return presenceListeners.add(listener);
  1739.     }

  1740.     /**
  1741.      * Removes a stanza listener that was being notified of any new Presence packets
  1742.      * sent to the group chat.
  1743.      *
  1744.      * @param listener a stanza listener that was being notified of any presence packets
  1745.      *      sent to the group chat.
  1746.      * @return true if the listener was removed, otherwise the listener was not added previously.
  1747.      */
  1748.     public boolean removeParticipantListener(PresenceListener listener) {
  1749.         return presenceListeners.remove(listener);
  1750.     }

  1751.     /**
  1752.      * Returns a list of <code>Affiliate</code> with the room owners.
  1753.      *
  1754.      * @return a list of <code>Affiliate</code> with the room owners.
  1755.      * @throws XMPPErrorException if you don't have enough privileges to get this information.
  1756.      * @throws NoResponseException if there was no response from the server.
  1757.      * @throws NotConnectedException if the XMPP connection is not connected.
  1758.      * @throws InterruptedException if the calling thread was interrupted.
  1759.      */
  1760.     public List<Affiliate> getOwners() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  1761.         return getAffiliatesByAdmin(MUCAffiliation.owner);
  1762.     }

  1763.     /**
  1764.      * Returns a list of <code>Affiliate</code> with the room administrators.
  1765.      *
  1766.      * @return a list of <code>Affiliate</code> with the room administrators.
  1767.      * @throws XMPPErrorException if you don't have enough privileges to get this information.
  1768.      * @throws NoResponseException if there was no response from the server.
  1769.      * @throws NotConnectedException if the XMPP connection is not connected.
  1770.      * @throws InterruptedException if the calling thread was interrupted.
  1771.      */
  1772.     public List<Affiliate> getAdmins() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  1773.         return getAffiliatesByAdmin(MUCAffiliation.admin);
  1774.     }

  1775.     /**
  1776.      * Returns a list of <code>Affiliate</code> with the room members.
  1777.      *
  1778.      * @return a list of <code>Affiliate</code> with the room members.
  1779.      * @throws XMPPErrorException if you don't have enough privileges to get this information.
  1780.      * @throws NoResponseException if there was no response from the server.
  1781.      * @throws NotConnectedException if the XMPP connection is not connected.
  1782.      * @throws InterruptedException if the calling thread was interrupted.
  1783.      */
  1784.     public List<Affiliate> getMembers() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException  {
  1785.         return getAffiliatesByAdmin(MUCAffiliation.member);
  1786.     }

  1787.     /**
  1788.      * Returns a list of <code>Affiliate</code> with the room outcasts.
  1789.      *
  1790.      * @return a list of <code>Affiliate</code> with the room outcasts.
  1791.      * @throws XMPPErrorException if you don't have enough privileges to get this information.
  1792.      * @throws NoResponseException if there was no response from the server.
  1793.      * @throws NotConnectedException if the XMPP connection is not connected.
  1794.      * @throws InterruptedException if the calling thread was interrupted.
  1795.      */
  1796.     public List<Affiliate> getOutcasts() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  1797.         return getAffiliatesByAdmin(MUCAffiliation.outcast);
  1798.     }

  1799.     /**
  1800.      * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
  1801.      * sending a request in the admin namespace.
  1802.      *
  1803.      * @param affiliation the affiliation of the users in the room.
  1804.      * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
  1805.      * @throws XMPPErrorException if you don't have enough privileges to get this information.
  1806.      * @throws NoResponseException if there was no response from the server.
  1807.      * @throws NotConnectedException if the XMPP connection is not connected.
  1808.      * @throws InterruptedException if the calling thread was interrupted.
  1809.      */
  1810.     private List<Affiliate> getAffiliatesByAdmin(MUCAffiliation affiliation) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  1811.         MUCAdmin iq = new MUCAdmin();
  1812.         iq.setTo(room);
  1813.         iq.setType(IQ.Type.get);
  1814.         // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
  1815.         MUCItem item = new MUCItem(affiliation);
  1816.         iq.addItem(item);

  1817.         MUCAdmin answer = (MUCAdmin) connection.sendIqRequestAndWaitForResponse(iq);

  1818.         // Get the list of affiliates from the server's answer
  1819.         List<Affiliate> affiliates = new ArrayList<Affiliate>();
  1820.         for (MUCItem mucadminItem : answer.getItems()) {
  1821.             affiliates.add(new Affiliate(mucadminItem));
  1822.         }
  1823.         return affiliates;
  1824.     }

  1825.     /**
  1826.      * Returns a list of <code>Occupant</code> with the room moderators.
  1827.      *
  1828.      * @return a list of <code>Occupant</code> with the room moderators.
  1829.      * @throws XMPPErrorException if you don't have enough privileges to get this information.
  1830.      * @throws NoResponseException if there was no response from the server.
  1831.      * @throws NotConnectedException if the XMPP connection is not connected.
  1832.      * @throws InterruptedException if the calling thread was interrupted.
  1833.      */
  1834.     public List<Occupant> getModerators() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  1835.         return getOccupants(MUCRole.moderator);
  1836.     }

  1837.     /**
  1838.      * Returns a list of <code>Occupant</code> with the room participants.
  1839.      *
  1840.      * @return a list of <code>Occupant</code> with the room participants.
  1841.      * @throws XMPPErrorException if you don't have enough privileges to get this information.
  1842.      * @throws NoResponseException if there was no response from the server.
  1843.      * @throws NotConnectedException if the XMPP connection is not connected.
  1844.      * @throws InterruptedException if the calling thread was interrupted.
  1845.      */
  1846.     public List<Occupant> getParticipants() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  1847.         return getOccupants(MUCRole.participant);
  1848.     }

  1849.     /**
  1850.      * Returns a list of <code>Occupant</code> that have the specified room role.
  1851.      *
  1852.      * @param role the role of the occupant in the room.
  1853.      * @return a list of <code>Occupant</code> that have the specified room role.
  1854.      * @throws XMPPErrorException if an error occurred while performing the request to the server or you
  1855.      *         don't have enough privileges to get this information.
  1856.      * @throws NoResponseException if there was no response from the server.
  1857.      * @throws NotConnectedException if the XMPP connection is not connected.
  1858.      * @throws InterruptedException if the calling thread was interrupted.
  1859.      */
  1860.     private List<Occupant> getOccupants(MUCRole role) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  1861.         MUCAdmin iq = new MUCAdmin();
  1862.         iq.setTo(room);
  1863.         iq.setType(IQ.Type.get);
  1864.         // Set the specified role. This may request the list of moderators/participants.
  1865.         MUCItem item = new MUCItem(role);
  1866.         iq.addItem(item);

  1867.         MUCAdmin answer = (MUCAdmin) connection.sendIqRequestAndWaitForResponse(iq);
  1868.         // Get the list of participants from the server's answer
  1869.         List<Occupant> participants = new ArrayList<Occupant>();
  1870.         for (MUCItem mucadminItem : answer.getItems()) {
  1871.             participants.add(new Occupant(mucadminItem));
  1872.         }
  1873.         return participants;
  1874.     }

  1875.     /**
  1876.      * Sends a message to the chat room.
  1877.      *
  1878.      * @param text the text of the message to send.
  1879.      * @throws NotConnectedException if the XMPP connection is not connected.
  1880.      * @throws InterruptedException if the calling thread was interrupted.
  1881.      */
  1882.     public void sendMessage(String text) throws NotConnectedException, InterruptedException {
  1883.         Message message = buildMessage()
  1884.                 .setBody(text)
  1885.                 .build();
  1886.         connection.sendStanza(message);
  1887.     }

  1888.     /**
  1889.      * Returns a new Chat for sending private messages to a given room occupant.
  1890.      * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
  1891.      * service will change the 'from' address to the sender's room JID and delivering the message
  1892.      * to the intended recipient's full JID.
  1893.      *
  1894.      * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul').
  1895.      * @param listener the listener is a message listener that will handle messages for the newly
  1896.      * created chat.
  1897.      * @return new Chat for sending private messages to a given room occupant.
  1898.      */
  1899.     // 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.
  1900.     // API sketch: PrivateMucChat createPrivateChat(Resourcepart nick)
  1901.     @SuppressWarnings("deprecation")
  1902.     public org.jivesoftware.smack.chat.Chat createPrivateChat(EntityFullJid occupant, ChatMessageListener listener) {
  1903.         return org.jivesoftware.smack.chat.ChatManager.getInstanceFor(connection).createChat(occupant, listener);
  1904.     }

  1905.     /**
  1906.      * Creates a new Message to send to the chat room.
  1907.      *
  1908.      * @return a new Message addressed to the chat room.
  1909.      * @deprecated use {@link #buildMessage()} instead.
  1910.      */
  1911.     @Deprecated
  1912.     // TODO: Remove when stanza builder is ready.
  1913.     public Message createMessage() {
  1914.         return connection.getStanzaFactory().buildMessageStanza()
  1915.                 .ofType(Message.Type.groupchat)
  1916.                 .to(room)
  1917.                 .build();
  1918.     }

  1919.     /**
  1920.      * Constructs a new message builder for messages send to this MUC room.
  1921.      *
  1922.      * @return a new message builder.
  1923.      */
  1924.     public MessageBuilder buildMessage() {
  1925.         return connection.getStanzaFactory()
  1926.                 .buildMessageStanza()
  1927.                 .ofType(Message.Type.groupchat)
  1928.                 .to(room)
  1929.                 ;
  1930.     }

  1931.     /**
  1932.      * Sends a Message to the chat room.
  1933.      *
  1934.      * @param messageBuilder the message.
  1935.      * @return a read-only view of the send message.
  1936.      * @throws NotConnectedException if the XMPP connection is not connected.
  1937.      * @throws InterruptedException if the calling thread was interrupted.
  1938.      */
  1939.     public MessageView sendMessage(MessageBuilder messageBuilder) throws NotConnectedException, InterruptedException {
  1940.         for (MucMessageInterceptor interceptor : messageInterceptors) {
  1941.             interceptor.intercept(messageBuilder, this);
  1942.         }

  1943.         Message message = messageBuilder.to(room).ofType(Message.Type.groupchat).build();
  1944.         connection.sendStanza(message);
  1945.         return message;
  1946.     }

  1947.     /**
  1948.     * Polls for and returns the next message, or <code>null</code> if there isn't
  1949.     * a message immediately available. This method provides significantly different
  1950.     * functionality than the {@link #nextMessage()} method since it's non-blocking.
  1951.     * In other words, the method call will always return immediately, whereas the
  1952.     * nextMessage method will return only when a message is available (or after
  1953.     * a specific timeout).
  1954.     *
  1955.     * @return the next message if one is immediately available and
  1956.     *      <code>null</code> otherwise.
  1957.      * @throws MucNotJoinedException if not joined to the Multi-User Chat.
  1958.     */
  1959.     public Message pollMessage() throws MucNotJoinedException {
  1960.         if (messageCollector == null) {
  1961.             throw new MucNotJoinedException(this);
  1962.         }
  1963.         return messageCollector.pollResult();
  1964.     }

  1965.     /**
  1966.      * Returns the next available message in the chat. The method call will block
  1967.      * (not return) until a message is available.
  1968.      *
  1969.      * @return the next message.
  1970.      * @throws MucNotJoinedException if not joined to the Multi-User Chat.
  1971.      * @throws InterruptedException if the calling thread was interrupted.
  1972.      */
  1973.     public Message nextMessage() throws MucNotJoinedException, InterruptedException {
  1974.         if (messageCollector == null) {
  1975.             throw new MucNotJoinedException(this);
  1976.         }
  1977.         return  messageCollector.nextResultBlockForever();
  1978.     }

  1979.     /**
  1980.      * Returns the next available message in the chat. The method call will block
  1981.      * (not return) until a stanza is available or the <code>timeout</code> has elapsed.
  1982.      * If the timeout elapses without a result, <code>null</code> will be returned.
  1983.      *
  1984.      * @param timeout the maximum amount of time to wait for the next message.
  1985.      * @return the next message, or <code>null</code> if the timeout elapses without a
  1986.      *      message becoming available.
  1987.      * @throws MucNotJoinedException if not joined to the Multi-User Chat.
  1988.      * @throws InterruptedException if the calling thread was interrupted.
  1989.      */
  1990.     public Message nextMessage(long timeout) throws MucNotJoinedException, InterruptedException {
  1991.         if (messageCollector == null) {
  1992.             throw new MucNotJoinedException(this);
  1993.         }
  1994.         return messageCollector.nextResult(timeout);
  1995.     }

  1996.     /**
  1997.      * Adds a stanza listener that will be notified of any new messages in the
  1998.      * group chat. Only "group chat" messages addressed to this group chat will
  1999.      * be delivered to the listener. If you wish to listen for other packets
  2000.      * that may be associated with this group chat, you should register a
  2001.      * PacketListener directly with the XMPPConnection with the appropriate
  2002.      * PacketListener.
  2003.      *
  2004.      * @param listener a stanza listener.
  2005.      * @return true if the listener was not already added.
  2006.      */
  2007.     public boolean addMessageListener(MessageListener listener) {
  2008.         return messageListeners.add(listener);
  2009.     }

  2010.     /**
  2011.      * Removes a stanza listener that was being notified of any new messages in the
  2012.      * multi user chat. Only "group chat" messages addressed to this multi user chat were
  2013.      * being delivered to the listener.
  2014.      *
  2015.      * @param listener a stanza listener.
  2016.      * @return true if the listener was removed, otherwise the listener was not added previously.
  2017.      */
  2018.     public boolean removeMessageListener(MessageListener listener) {
  2019.         return messageListeners.remove(listener);
  2020.     }

  2021.     public boolean addMessageInterceptor(MucMessageInterceptor interceptor) {
  2022.         return messageInterceptors.add(interceptor);
  2023.     }

  2024.     public boolean removeMessageInterceptor(MucMessageInterceptor interceptor) {
  2025.         return messageInterceptors.remove(interceptor);
  2026.     }

  2027.     /**
  2028.      * Changes the subject within the room. As a default, only users with a role of "moderator"
  2029.      * are allowed to change the subject in a room. Although some rooms may be configured to
  2030.      * allow a mere participant or even a visitor to change the subject.
  2031.      *
  2032.      * @param subject the new room's subject to set.
  2033.      * @throws XMPPErrorException if someone without appropriate privileges attempts to change the
  2034.      *          room subject will throw an error with code 403 (i.e. Forbidden)
  2035.      * @throws NoResponseException if there was no response from the server.
  2036.      * @throws NotConnectedException if the XMPP connection is not connected.
  2037.      * @throws InterruptedException if the calling thread was interrupted.
  2038.      */
  2039.     public void changeSubject(final String subject) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
  2040.         Message message = buildMessage()
  2041.                         .setSubject(subject)
  2042.                         .build();
  2043.         // Wait for an error or confirmation message back from the server.
  2044.         StanzaFilter successFilter = new AndFilter(fromRoomGroupchatFilter, new StanzaFilter() {
  2045.             @Override
  2046.             public boolean accept(Stanza packet) {
  2047.                 Message msg = (Message) packet;
  2048.                 return subject.equals(msg.getSubject());
  2049.             }
  2050.         });
  2051.         StanzaFilter errorFilter = new AndFilter(fromRoomFilter, new StanzaIdFilter(message), MessageTypeFilter.ERROR);
  2052.         StanzaFilter responseFilter = new OrFilter(successFilter, errorFilter);
  2053.         StanzaCollector response = connection.createStanzaCollectorAndSend(responseFilter, message);
  2054.         // Wait up to a certain number of seconds for a reply.
  2055.         response.nextResultOrThrow();
  2056.     }

  2057.     /**
  2058.      * Remove the connection callbacks (PacketListener, PacketInterceptor, StanzaCollector) used by this MUC from the
  2059.      * connection.
  2060.      */
  2061.     private void removeConnectionCallbacks() {
  2062.         connection.removeStanzaListener(messageListener);
  2063.         connection.removeStanzaListener(presenceListener);
  2064.         connection.removeStanzaListener(subjectListener);
  2065.         connection.removeStanzaListener(declinesListener);
  2066.         connection.removePresenceInterceptor(presenceInterceptor);
  2067.         if (messageCollector != null) {
  2068.             messageCollector.cancel();
  2069.             messageCollector = null;
  2070.         }
  2071.     }

  2072.     /**
  2073.      * Remove all callbacks and resources necessary when the user has left the room for some reason.
  2074.      */
  2075.     private synchronized void userHasLeft() {
  2076.         occupantsMap.clear();
  2077.         myRoomJid = null;
  2078.         // Update the list of joined rooms
  2079.         multiUserChatManager.removeJoinedRoom(room);
  2080.         removeConnectionCallbacks();
  2081.     }

  2082.     /**
  2083.      * Adds a listener that will be notified of changes in your status in the room
  2084.      * such as the user being kicked, banned, or granted admin permissions.
  2085.      *
  2086.      * @param listener a user status listener.
  2087.      * @return true if the user status listener was not already added.
  2088.      */
  2089.     public boolean addUserStatusListener(UserStatusListener listener) {
  2090.         return userStatusListeners.add(listener);
  2091.     }

  2092.     /**
  2093.      * Removes a listener that was being notified of changes in your status in the room
  2094.      * such as the user being kicked, banned, or granted admin permissions.
  2095.      *
  2096.      * @param listener a user status listener.
  2097.      * @return true if the listener was registered and is now removed.
  2098.      */
  2099.     public boolean removeUserStatusListener(UserStatusListener listener) {
  2100.         return userStatusListeners.remove(listener);
  2101.     }

  2102.     /**
  2103.      * Adds a listener that will be notified of changes in occupants status in the room
  2104.      * such as the user being kicked, banned, or granted admin permissions.
  2105.      *
  2106.      * @param listener a participant status listener.
  2107.      * @return true if the listener was not already added.
  2108.      */
  2109.     public boolean addParticipantStatusListener(ParticipantStatusListener listener) {
  2110.         return participantStatusListeners.add(listener);
  2111.     }

  2112.     /**
  2113.      * Removes a listener that was being notified of changes in occupants status in the room
  2114.      * such as the user being kicked, banned, or granted admin permissions.
  2115.      *
  2116.      * @param listener a participant status listener.
  2117.      * @return true if the listener was registered and is now removed.
  2118.      */
  2119.     public boolean removeParticipantStatusListener(ParticipantStatusListener listener) {
  2120.         return participantStatusListeners.remove(listener);
  2121.     }

  2122.     /**
  2123.      * Fires notification events if the role of a room occupant has changed. If the occupant that
  2124.      * changed his role is your occupant then the <code>UserStatusListeners</code> added to this
  2125.      * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
  2126.      * his role is not yours then the <code>ParticipantStatusListeners</code> added to this
  2127.      * <code>MultiUserChat</code> will be fired. The following table shows the events that will
  2128.      * be fired depending on the previous and new role of the occupant.
  2129.      *
  2130.      * <pre>
  2131.      * <table border="1">
  2132.      * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
  2133.      *
  2134.      * <tr><td>None</td><td>Visitor</td><td>--</td></tr>
  2135.      * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
  2136.      * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
  2137.      *
  2138.      * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
  2139.      * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
  2140.      * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
  2141.      *
  2142.      * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
  2143.      * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
  2144.      * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
  2145.      *
  2146.      * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
  2147.      * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
  2148.      * <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
  2149.      * </table>
  2150.      * </pre>
  2151.      *
  2152.      * @param oldRole the previous role of the user in the room before receiving the new presence
  2153.      * @param newRole the new role of the user in the room after receiving the new presence
  2154.      * @param isUserModification whether the received presence is about your user in the room or not
  2155.      * @param from the occupant whose role in the room has changed
  2156.      * (e.g. room@conference.jabber.org/nick).
  2157.      */
  2158.     private void checkRoleModifications(
  2159.         MUCRole oldRole,
  2160.         MUCRole newRole,
  2161.         boolean isUserModification,
  2162.         EntityFullJid from) {
  2163.         // Voice was granted to a visitor
  2164.         if ((MUCRole.visitor.equals(oldRole) || MUCRole.none.equals(oldRole))
  2165.             && MUCRole.participant.equals(newRole)) {
  2166.             if (isUserModification) {
  2167.                 for (UserStatusListener listener : userStatusListeners) {
  2168.                     listener.voiceGranted();
  2169.                 }
  2170.             }
  2171.             else {
  2172.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2173.                     listener.voiceGranted(from);
  2174.                 }
  2175.             }
  2176.         }
  2177.         // The participant's voice was revoked from the room
  2178.         else if (
  2179.             MUCRole.participant.equals(oldRole)
  2180.                 && (MUCRole.visitor.equals(newRole) || MUCRole.none.equals(newRole))) {
  2181.             if (isUserModification) {
  2182.                 for (UserStatusListener listener : userStatusListeners) {
  2183.                     listener.voiceRevoked();
  2184.                 }
  2185.             }
  2186.             else {
  2187.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2188.                     listener.voiceRevoked(from);
  2189.                 }
  2190.             }
  2191.         }
  2192.         // Moderator privileges were granted to a participant
  2193.         if (!MUCRole.moderator.equals(oldRole) && MUCRole.moderator.equals(newRole)) {
  2194.             if (MUCRole.visitor.equals(oldRole) || MUCRole.none.equals(oldRole)) {
  2195.                 if (isUserModification) {
  2196.                     for (UserStatusListener listener : userStatusListeners) {
  2197.                         listener.voiceGranted();
  2198.                     }
  2199.                 }
  2200.                 else {
  2201.                     for (ParticipantStatusListener listener : participantStatusListeners) {
  2202.                         listener.voiceGranted(from);
  2203.                     }
  2204.                 }
  2205.             }
  2206.             if (isUserModification) {
  2207.                 for (UserStatusListener listener : userStatusListeners) {
  2208.                     listener.moderatorGranted();
  2209.                 }
  2210.             }
  2211.             else {
  2212.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2213.                     listener.moderatorGranted(from);
  2214.                 }
  2215.             }
  2216.         }
  2217.         // Moderator privileges were revoked from a participant
  2218.         else if (MUCRole.moderator.equals(oldRole) && !MUCRole.moderator.equals(newRole)) {
  2219.             if (MUCRole.visitor.equals(newRole) || MUCRole.none.equals(newRole)) {
  2220.                 if (isUserModification) {
  2221.                     for (UserStatusListener listener : userStatusListeners) {
  2222.                         listener.voiceRevoked();
  2223.                     }
  2224.                 }
  2225.                 else {
  2226.                     for (ParticipantStatusListener listener : participantStatusListeners) {
  2227.                         listener.voiceRevoked(from);
  2228.                     }
  2229.                 }
  2230.             }
  2231.             if (isUserModification) {
  2232.                 for (UserStatusListener listener : userStatusListeners) {
  2233.                     listener.moderatorRevoked();
  2234.                 }
  2235.             }
  2236.             else {
  2237.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2238.                     listener.moderatorRevoked(from);
  2239.                 }
  2240.             }
  2241.         }
  2242.     }

  2243.     /**
  2244.      * Fires notification events if the affiliation of a room occupant has changed. If the
  2245.      * occupant that changed his affiliation is your occupant then the
  2246.      * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
  2247.      * On the other hand, if the occupant that changed his affiliation is not yours then the
  2248.      * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
  2249.      * fired. The following table shows the events that will be fired depending on the previous
  2250.      * and new affiliation of the occupant.
  2251.      *
  2252.      * <pre>
  2253.      * <table border="1">
  2254.      * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
  2255.      *
  2256.      * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
  2257.      * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
  2258.      * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
  2259.      *
  2260.      * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
  2261.      * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
  2262.      * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
  2263.      *
  2264.      * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
  2265.      * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
  2266.      * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
  2267.      *
  2268.      * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
  2269.      * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
  2270.      * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
  2271.      * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
  2272.      * </table>
  2273.      * </pre>
  2274.      *
  2275.      * @param oldAffiliation the previous affiliation of the user in the room before receiving the
  2276.      * new presence
  2277.      * @param newAffiliation the new affiliation of the user in the room after receiving the new
  2278.      * presence
  2279.      * @param isUserModification whether the received presence is about your user in the room or not
  2280.      * @param from the occupant whose role in the room has changed
  2281.      * (e.g. room@conference.jabber.org/nick).
  2282.      */
  2283.     private void checkAffiliationModifications(
  2284.         MUCAffiliation oldAffiliation,
  2285.         MUCAffiliation newAffiliation,
  2286.         boolean isUserModification,
  2287.         EntityFullJid from) {
  2288.         // First check for revoked affiliation and then for granted affiliations. The idea is to
  2289.         // first fire the "revoke" events and then fire the "grant" events.

  2290.         // The user's ownership to the room was revoked
  2291.         if (MUCAffiliation.owner.equals(oldAffiliation) && !MUCAffiliation.owner.equals(newAffiliation)) {
  2292.             if (isUserModification) {
  2293.                 for (UserStatusListener listener : userStatusListeners) {
  2294.                     listener.ownershipRevoked();
  2295.                 }
  2296.             }
  2297.             else {
  2298.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2299.                     listener.ownershipRevoked(from);
  2300.                 }
  2301.             }
  2302.         }
  2303.         // The user's administrative privileges to the room were revoked
  2304.         else if (MUCAffiliation.admin.equals(oldAffiliation) && !MUCAffiliation.admin.equals(newAffiliation)) {
  2305.             if (isUserModification) {
  2306.                 for (UserStatusListener listener : userStatusListeners) {
  2307.                     listener.adminRevoked();
  2308.                 }
  2309.             }
  2310.             else {
  2311.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2312.                     listener.adminRevoked(from);
  2313.                 }
  2314.             }
  2315.         }
  2316.         // The user's membership to the room was revoked
  2317.         else if (MUCAffiliation.member.equals(oldAffiliation) && !MUCAffiliation.member.equals(newAffiliation)) {
  2318.             if (isUserModification) {
  2319.                 for (UserStatusListener listener : userStatusListeners) {
  2320.                     listener.membershipRevoked();
  2321.                 }
  2322.             }
  2323.             else {
  2324.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2325.                     listener.membershipRevoked(from);
  2326.                 }
  2327.             }
  2328.         }

  2329.         // The user was granted ownership to the room
  2330.         if (!MUCAffiliation.owner.equals(oldAffiliation) && MUCAffiliation.owner.equals(newAffiliation)) {
  2331.             if (isUserModification) {
  2332.                 for (UserStatusListener listener : userStatusListeners) {
  2333.                     listener.ownershipGranted();
  2334.                 }
  2335.             }
  2336.             else {
  2337.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2338.                     listener.ownershipGranted(from);
  2339.                 }
  2340.             }
  2341.         }
  2342.         // The user was granted administrative privileges to the room
  2343.         else if (!MUCAffiliation.admin.equals(oldAffiliation) && MUCAffiliation.admin.equals(newAffiliation)) {
  2344.             if (isUserModification) {
  2345.                 for (UserStatusListener listener : userStatusListeners) {
  2346.                     listener.adminGranted();
  2347.                 }
  2348.             }
  2349.             else {
  2350.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2351.                     listener.adminGranted(from);
  2352.                 }
  2353.             }
  2354.         }
  2355.         // The user was granted membership to the room
  2356.         else if (!MUCAffiliation.member.equals(oldAffiliation) && MUCAffiliation.member.equals(newAffiliation)) {
  2357.             if (isUserModification) {
  2358.                 for (UserStatusListener listener : userStatusListeners) {
  2359.                     listener.membershipGranted();
  2360.                 }
  2361.             }
  2362.             else {
  2363.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2364.                     listener.membershipGranted(from);
  2365.                 }
  2366.             }
  2367.         }
  2368.     }

  2369.     /**
  2370.      * Fires events according to the received presence code.
  2371.      *
  2372.      * @param statusCodes TODO javadoc me please
  2373.      * @param isUserModification TODO javadoc me please
  2374.      * @param mucUser TODO javadoc me please
  2375.      * @param from TODO javadoc me please
  2376.      */
  2377.     private void checkPresenceCode(
  2378.         Set<Status> statusCodes,
  2379.         boolean isUserModification,
  2380.         MUCUser mucUser,
  2381.         EntityFullJid from) {
  2382.         // Check if an occupant was kicked from the room
  2383.         if (statusCodes.contains(Status.KICKED_307)) {
  2384.             // Check if this occupant was kicked
  2385.             if (isUserModification) {
  2386.                 for (UserStatusListener listener : userStatusListeners) {
  2387.                     listener.kicked(mucUser.getItem().getActor(), mucUser.getItem().getReason());
  2388.                 }
  2389.             }
  2390.             else {
  2391.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2392.                     listener.kicked(from, mucUser.getItem().getActor(), mucUser.getItem().getReason());
  2393.                 }
  2394.             }
  2395.         }
  2396.         // A user was banned from the room
  2397.         if (statusCodes.contains(Status.BANNED_301)) {
  2398.             // Check if this occupant was banned
  2399.             if (isUserModification) {
  2400.                 for (UserStatusListener listener : userStatusListeners) {
  2401.                     listener.banned(mucUser.getItem().getActor(), mucUser.getItem().getReason());
  2402.                 }
  2403.             }
  2404.             else {
  2405.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2406.                     listener.banned(from, mucUser.getItem().getActor(), mucUser.getItem().getReason());
  2407.                 }
  2408.             }
  2409.         }
  2410.         // A user's membership was revoked from the room
  2411.         if (statusCodes.contains(Status.REMOVED_AFFIL_CHANGE_321)) {
  2412.             // Check if this occupant's membership was revoked
  2413.             if (isUserModification) {
  2414.                 for (UserStatusListener listener : userStatusListeners) {
  2415.                     listener.membershipRevoked();
  2416.                 }
  2417.             } else {
  2418.                 for (ParticipantStatusListener listener : participantStatusListeners) {
  2419.                     listener.membershipRevoked(from);
  2420.                 }
  2421.             }
  2422.         }
  2423.         // A occupant has changed his nickname in the room
  2424.         if (statusCodes.contains(Status.NEW_NICKNAME_303)) {
  2425.             for (ParticipantStatusListener listener : participantStatusListeners) {
  2426.                 listener.nicknameChanged(from, mucUser.getItem().getNick());
  2427.             }
  2428.         }
  2429.     }

  2430.     /**
  2431.      * Get the XMPP connection associated with this chat instance.
  2432.      *
  2433.      * @return the associated XMPP connection.
  2434.      * @since 4.3.0
  2435.      */
  2436.     public XMPPConnection getXmppConnection() {
  2437.         return connection;
  2438.     }

  2439.     public boolean serviceSupportsStableIds() {
  2440.         return DiscoverInfo.nullSafeContainsFeature(mucServiceDiscoInfo, MultiUserChatConstants.STABLE_ID_FEATURE);
  2441.     }

  2442.     @Override
  2443.     public String toString() {
  2444.         return "MUC: " + room + "(" + connection.getUser() + ")";
  2445.     }
  2446. }