public MUCOccupant joinRoom(@Nonnull String nickname,
                                @Nullable String password,
                                @Nullable HistoryRequest historyRequest,
                                @Nonnull JID realAddress,
                                @Nonnull Presence presence )
        throws UnauthorizedException, UserAlreadyExistsException, RoomLockedException, ForbiddenException,
        RegistrationRequiredException, ConflictException, ServiceUnavailableException, NotAcceptableException
    {
        Log.debug( "User '{}' attempts to join room '{}' using nickname '{}'.", realAddress, this.getJID(), nickname );
        MUCOccupant joiningOccupant;
        boolean clientOnlyJoin; // A "client only join" here is one where the client is already joined, but has re-joined.

        synchronized (this) {
            // Determine the corresponding role based on the user's affiliation
            final JID bareJID = realAddress.asBareJID();
            Role role = getRole( bareJID );
            Affiliation affiliation = getAffiliation( bareJID );
            Log.debug( "User '{}' role and affiliation in room '{} are determined to be: {}, {}", realAddress, this.getJID(), role, affiliation );

            // Verify that the attempt meets all preconditions for joining the room.
            checkJoinRoomPreconditions( realAddress, nickname, affiliation, password, presence );

            // Is this client already joined with this nickname?
            clientOnlyJoin = alreadyJoinedWithThisNick( realAddress, nickname );

            // TODO up to this point, room state has not been modified, even though a write-lock has been acquired. Can we optimize concurrency by locking with only a read-lock up until here?
            if (!clientOnlyJoin)
            {
                Log.debug( "Adding user '{}' as an occupant of room '{}' using nickname '{}'.", realAddress, this.getJID(), nickname );

                // Create a new occupant for this user in this room.
                joiningOccupant = new MUCOccupant(this, nickname, role, affiliation, realAddress, presence);

                // See if we need to join a federated room. Note that this can be blocking!
                final Future<?> join = fmucHandler.join(joiningOccupant);
                try
                {
                    // FIXME make this properly asynchronous, instead of blocking the thread!
                    join.get(5, TimeUnit.MINUTES);
                }
                catch ( InterruptedException | ExecutionException | TimeoutException e )
                {
                    Log.error( "An exception occurred while processing FMUC join for user '{}' in room '{}'", joiningOccupant.getUserAddress(), this.getJID(), e);
                }

                addOccupant(joiningOccupant);

            } else {
                // Grab the existing one.
                Log.debug( "Skip adding user '{}' as an occupant of room '{}' using nickname '{}', as it already is. Updating occupancy with its latest presence information.", realAddress, this.getJID(), nickname );
                joiningOccupant = getOccupantByFullJID(realAddress);
                joiningOccupant.setPresence(presence); // OF-1581: Use the latest presence information.
            }
        }

        // Exchange initial presence information between occupants of the room.
        sendInitialPresencesToNewOccupant( joiningOccupant );

        // OF-2042: XEP dictates an order of events. Wait for the presence exchange to finish, before progressing.
        final CompletableFuture<Void> future = sendInitialPresenceToExistingOccupants(joiningOccupant);
        try {
            final Duration timeout = SELF_PRESENCE_TIMEOUT.getValue();
            future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
        } catch ( InterruptedException e ) {
            Log.debug( "Presence broadcast has been interrupted before it completed. Will continue to process the join of occupant '{}' to room '{}' as if it has.", joiningOccupant.getUserAddress(), this.getJID(), e);
        } catch ( TimeoutException e ) {
            Log.warn( "Presence broadcast has not yet been completed within the allocated period. Will continue to process the join of occupant '{}' to room '{}' as if it has.", joiningOccupant.getUserAddress(), this.getJID(), e);
        } catch ( ExecutionException e ) {
            Log.warn( "Presence broadcast caused an exception. Will continue to process the join of occupant '{}' to room '{}' as if it has.", joiningOccupant.getUserAddress(), this.getJID(), e);
        }

        // If the room has just been created send the "room locked until configuration is confirmed" message.
        // It is assumed that the room is new based on the fact that it's locked and that it was locked when it was created.
        final boolean isRoomNew = isLocked() && creationDate.getTime() == lockedTime;
        if (!isRoomNew && isLocked()) {
            // TODO Verify if it's right that this check occurs only _after_ a join was deemed 'successful' and initial presences have been exchanged.
            // http://xmpp.org/extensions/xep-0045.html#enter-locked
            Log.debug( "User '{}' attempts to join room '{}' that is locked (pending configuration confirmation). Sending an error.", realAddress, this.getJID() );
            final Presence presenceItemNotFound = new Presence(Presence.Type.error);
            presenceItemNotFound.setError(PacketError.Condition.item_not_found);
            presenceItemNotFound.setFrom(selfOccupantData.getOccupantJID());

            // Not needed to create a defensive copy of the stanza. It's not used anywhere else.
            joiningOccupant.send(presenceItemNotFound);
        }

        sendRoomHistoryAfterJoin( realAddress, joiningOccupant, historyRequest );
        sendRoomSubjectAfterJoin( realAddress, joiningOccupant );

        if (!clientOnlyJoin) {
            // Update the date when the last occupant left the room
            setEmptyDate(null);
        }
        return joiningOccupant;
    }
Joins the room using the given nickname.

@param nickname       The nickname the user wants to use in the chatroom  (cannot be {@code null}).
@param password       The password provided by the user to enter the chatroom or null if none.
@param historyRequest The amount of history that the user request or null meaning default.
@param realAddress    The 'real' (non-room) JID of the user that is joining (cannot be {@code null}).
@param presence       The presence sent by the user to join the room (cannot be {@code null}).
@return The occupant data created for the user.
@throws UnauthorizedException         If the user doesn't have permission to join the room.
@throws UserAlreadyExistsException    If the nickname is already taken.
@throws RoomLockedException           If the user is trying to join a locked room.
@throws ForbiddenException            If the user is an outcast.
@throws RegistrationRequiredException If the user is not a member of a members-only room.
@throws ConflictException             If another user attempts to join the room with a
                                      nickname reserved by the first user.
@throws ServiceUnavailableException   If the user cannot join the room since the max number
                                      of users has been reached.
@throws NotAcceptableException       If the registered user is trying to join with a
                                     nickname different from the reserved nickname.
xmppserver/src/main/java/org/jivesoftware/openfire/muc/MUCRoom.java