com.kixeye.kixmpp.server.module.muc.MucRoom.java Source code

Java tutorial

Introduction

Here is the source code for com.kixeye.kixmpp.server.module.muc.MucRoom.java

Source

package com.kixeye.kixmpp.server.module.muc;

/*
 * #%L
 * KIXMPP
 * %%
 * Copyright (C) 2014 KIXEYE, Inc
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import com.google.common.collect.Maps;
import com.kixeye.kixmpp.server.cluster.message.GetMucRoomNicknamesRequest;
import com.kixeye.kixmpp.server.cluster.message.RoomPresenceBroadcastTask;
import io.netty.channel.Channel;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;

import java.util.*;

import io.netty.util.concurrent.Promise;
import org.jdom2.Element;
import org.jdom2.Namespace;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.kixeye.kixmpp.KixmppJid;
import com.kixeye.kixmpp.date.XmppDateUtils;
import com.kixeye.kixmpp.server.cluster.message.RoomBroadcastTask;
import com.kixeye.kixmpp.server.module.bind.BindKixmppServerModule;

/**
 * A simple muc room.
 *
 * @author ebahtijaragic
 */
public class MucRoom {
    private final MucService service;
    private final KixmppJid roomJid;
    private final MucKixmppServerModule mucModule;
    private final String roomId;
    private final MucRoomSettings settings;

    private Map<KixmppJid, MucRole> jidRoles = new HashMap<>();
    private Map<KixmppJid, MucAffiliation> jidAffiliations = new HashMap<>();

    private Map<KixmppJid, String> nicknamesByBareJid = new HashMap<>();
    private Map<String, User> usersByNickname = Maps.newConcurrentMap();

    /**
     * @param service
     * @param roomJid
     * @param settings
     */
    public MucRoom(MucService service, KixmppJid roomJid, MucRoomSettings settings) {
        this.service = service;
        this.roomJid = roomJid;
        this.roomId = roomJid.getNode();
        this.mucModule = service.getServer().module(MucKixmppServerModule.class);
        this.settings = new MucRoomSettings(settings);
    }

    /**
     * Getter from roomJid
     *
     * @return
     */
    public KixmppJid getRoomJid() {
        return roomJid;
    }

    /**
     * Adds a user.
     * 
     * @param jid
     * @param nickname
     * @param role
     * @param affiliation
     */
    public void addUser(KixmppJid jid, String nickname, MucRole role, MucAffiliation affiliation) {
        jid = jid.withoutResource();
        checkForNicknameInUse(nickname, jid);

        nicknamesByBareJid.put(jid, nickname);
        jidRoles.put(jid, role);
        jidAffiliations.put(jid, affiliation);
    }

    /**
     * A user requets to join the room.
     * @param channel
     * @param nickname
     */
    public void join(Channel channel, String nickname) {
        join(channel, nickname, null);
    }

    /**
     * A user requests to join the room.
     *
     * @param channel
     * @param nickname
     * @param mucStanza
     */
    public void join(final Channel channel, String nickname, Element mucStanza) {
        KixmppJid jid = channel.attr(BindKixmppServerModule.JID).get();

        if (settings.isOpen() && !jidRoles.containsKey(jid.withoutResource())) {
            addUser(jid, nickname, MucRole.Participant, MucAffiliation.Member);
        }

        verifyMembership(jid.withoutResource());
        checkForNicknameInUse(nickname, jid);

        User user = usersByNickname.get(nickname);

        boolean existingUser = true;
        if (user == null) {
            user = new User(nickname, jid.withoutResource());
            usersByNickname.put(nickname, user);
            MucRoomEventHandler handler = service.getServer().getMucRoomEventHandler();
            if (handler != null) {
                handler.userAdded(this, user);
            }
            existingUser = false;
        }

        Client client = user.addClient(new Client(jid, nickname, channel));

        // xep-0045 7.2.3 begin
        // self presence
        KixmppJid fromRoomJid = roomJid.withResource(nickname);
        channel.writeAndFlush(createPresence(fromRoomJid, jid, MucRole.Participant, null));

        if (settings.isPresenceEnabled() && !existingUser) {
            // Send presence from existing occupants to new occupant
            sendExistingOccupantsPresenceToNewOccupant(user, channel);

            // Send new occupant's presence to all occupants
            broadcastPresence(fromRoomJid, MucRole.Participant, null);
        }
        // xep-0045 7.2.3 end

        if (settings.getSubject() != null) {
            Element message = new Element("message");
            message.setAttribute("id", UUID.randomUUID().toString());
            message.setAttribute("from", roomJid.withResource(nickname).toString());
            message.setAttribute("to", channel.attr(BindKixmppServerModule.JID).get().toString());
            message.setAttribute("type", "groupchat");

            message.addContent(new Element("subject").setText(settings.getSubject()));

            channel.writeAndFlush(message);
        }

        if (mucStanza != null) {
            Element history = mucStanza.getChild("history", mucStanza.getNamespace());

            if (history != null) {
                MucHistoryProvider historyProvider = mucModule.getHistoryProvider();

                if (historyProvider != null) {
                    Integer maxChars = null;
                    Integer maxStanzas = null;
                    Integer seconds = null;

                    String parsableString = history.getAttributeValue("maxchars");
                    if (parsableString != null) {
                        try {
                            maxChars = Integer.parseInt(parsableString);
                        } catch (Exception e) {
                        }
                    }
                    parsableString = history.getAttributeValue("maxstanzas");
                    if (parsableString != null) {
                        try {
                            maxStanzas = Integer.parseInt(parsableString);
                        } catch (Exception e) {
                        }
                    }
                    parsableString = history.getAttributeValue("seconds");
                    if (parsableString != null) {
                        try {
                            seconds = Integer.parseInt(parsableString);
                        } catch (Exception e) {
                        }
                    }

                    String since = history.getAttributeValue("since");

                    historyProvider.getHistory(roomJid, user.getBareJid(), maxChars, maxStanzas, seconds, since)
                            .addListener(new GenericFutureListener<Future<List<MucHistory>>>() {
                                @Override
                                public void operationComplete(Future<List<MucHistory>> future) throws Exception {
                                    if (future.isSuccess()) {
                                        List<MucHistory> historyItems = future.get();
                                        if (historyItems != null) {
                                            for (MucHistory historyItem : historyItems) {
                                                Element message = new Element("message")
                                                        .setAttribute("id", UUID.randomUUID().toString())
                                                        .setAttribute("from",
                                                                roomJid.withResource(historyItem.getNickname())
                                                                        .toString())
                                                        .setAttribute("to",
                                                                channel.attr(BindKixmppServerModule.JID).get()
                                                                        .toString())
                                                        .setAttribute("type", "groupchat");
                                                message.addContent(
                                                        new Element("body").setText(historyItem.getBody()));

                                                Element addresses = new Element("addresses", Namespace
                                                        .getNamespace("http://jabber.org/protocol/address"));
                                                addresses
                                                        .addContent(new Element("address", addresses.getNamespace())
                                                                .setAttribute("type", "ofrom").setAttribute("jid",
                                                                        historyItem.getFrom().toString()));
                                                message.addContent(addresses);

                                                message.addContent(new Element("delay",
                                                        Namespace.getNamespace("urn:xmpp:delay"))
                                                                .setAttribute("from", roomJid.toString())
                                                                .setAttribute("stamp", XmppDateUtils
                                                                        .format(historyItem.getTimestamp())));

                                                channel.write(message);
                                            }
                                            channel.flush();
                                        }
                                    }
                                }
                            });
                }
            }
        }

        channel.closeFuture().addListener(new CloseChannelListener(client));
    }

    private void sendExistingOccupantsPresenceToNewOccupant(final User newUser, final Channel channel) {

        final KixmppJid jid = channel.attr(BindKixmppServerModule.JID).get();
        final MucRole role = jidRoles.get(jid.withoutResource());

        Promise<Set<String>> promise = service.getServer().createPromise();
        promise.addListener(new GenericFutureListener<Future<Set<String>>>() {
            @Override
            public void operationComplete(Future<Set<String>> future) throws Exception {
                if (future.isSuccess()) {
                    Set<String> nicknames = future.get();
                    for (String nickname : nicknames) {

                        if (newUser.getNickname().equals(nickname)) {
                            continue;
                        }

                        Element presence = createPresence(roomJid.withResource(nickname), jid, role, null);
                        channel.write(presence);
                    }
                    if (!nicknames.isEmpty()) {
                        channel.flush();
                    }
                }
            }
        });
        service.getServer()
                .sendMapReduceRequest(new GetMucRoomNicknamesRequest(service.getSubDomain(), roomId, jid, promise));
    }

    private void broadcastPresence(KixmppJid fromRoomJid, MucRole role, String type) {
        receivePresence(fromRoomJid, role, type);
        service.getServer().getCluster().sendMessageToAll(
                new RoomPresenceBroadcastTask(this, service.getSubDomain(), roomId, fromRoomJid, role, type),
                false);
    }

    public void receivePresence(KixmppJid fromRoomJid, MucRole role, String type) {
        String nickname = fromRoomJid.getResource();
        for (User user : usersByNickname.values()) {
            if (user.getNickname().equals(nickname)) {
                continue;
            }
            user.receivePresence(fromRoomJid, role, type);
        }
    }

    private void checkForNicknameInUse(String nickname, KixmppJid jid) {
        User user = usersByNickname.get(nickname);
        if (user != null && !user.getBareJid().equals(jid.withoutResource())) {
            throw new NicknameInUseException(this, nickname);
        }
    }

    private void verifyMembership(KixmppJid jid) {
        MucAffiliation affiliation = jidAffiliations.get(jid);

        if (affiliation == null) {
            throw new RoomJoinNotAllowedException(this, jid);
        }

        MucRole role = jidRoles.get(jid);

        if (role == null) {
            switch (affiliation) {
            case Owner:
            case Admin:
                role = MucRole.Moderator;
                break;
            case Member:
                role = MucRole.Participant;
                break;
            case None:
                role = MucRole.Visitor;
                break;
            case Outcast:
                throw new RoomJoinNotAllowedException(this, jid);
            default:
                role = MucRole.None;
                break;
            }
        }

        jidRoles.put(jid, role);
        jidAffiliations.put(jid, affiliation);
    }

    /**
     * Remove the {@link User} associated with the given {@link KixmppJid} from the
     * room.  This will remove all {@link Client}s associated with the {@link User}.
     *
     * @param address
     * @return
     */
    public boolean removeUser(KixmppJid address) {
        KixmppJid bareJid = address.withoutResource();
        String nickname = nicknamesByBareJid.get(bareJid);
        if (nickname == null) {
            //user is not in the room
            return false;
        }
        User user = usersByNickname.get(nickname);
        if (user == null) {
            //user is no longer connected to the room
            return false;
        }
        user.removeClients();
        removeDisconnectedUser(user);
        return true;
    }

    public boolean userLeft(Channel channel, String nickname) {
        User user = usersByNickname.get(nickname);
        if (user == null) {
            //user not found
            return false;
        }
        Client client = user.getClient(channel);
        if (client == null) {
            //client not found
            return false;
        }
        user.removeClient(client);
        removeDisconnectedUser(user);

        return true;
    }

    /**
     * A user leaves the room.
     *
     * @param client
     */
    private void leave(Client client) {
        User user = usersByNickname.get(client.getNickname());

        if (user != null) {
            user.removeClient(client);
            removeDisconnectedUser(user);
        }
    }

    private Element createPresence(KixmppJid from, KixmppJid to, MucRole role, String type) {
        Element presence = new Element("presence");

        presence.setAttribute("id", UUID.randomUUID().toString());
        presence.setAttribute("from", from.toString());
        presence.setAttribute("to", to.toString());

        Element x = new Element("x", Namespace.getNamespace("http://jabber.org/protocol/muc#user"));

        if (type != null) {
            presence.setAttribute("type", type);
        }

        Element item = new Element("item", Namespace.getNamespace("http://jabber.org/protocol/muc#user"));
        item.setAttribute("affiliation", "member");

        switch (role) {
        case Participant:
            item.setAttribute("role", "participant");
            break;
        case Moderator:
            item.setAttribute("role", "moderator");
            break;
        case Visitor:
            item.setAttribute("role", "visitor");
            break;
        }
        x.addContent(item);
        presence.addContent(x);

        return presence;
    }

    /**
     * Broadcasts a message using supplied nickname.
     *
     * @param fromAddress
     * @param messages
     */
    public void receiveMessages(KixmppJid fromAddress, boolean sendToCluster, String... messages) {
        if (fromAddress == null) {
            return;
        }

        MucRole fromRole = jidRoles.get(fromAddress.withoutResource());

        if (fromRole == null) {
            removeUser(fromAddress);
            return;
        }

        switch (fromRole) {
        case None:
        case Visitor:
            // TODO maybe send back and error?
            return;
        default:
            break;
        }

        String fromNickname = nicknamesByBareJid.get(fromAddress.withoutResource());
        //TODO validate fromAddress is roomJid or is a member of the room
        KixmppJid fromRoomJid = roomJid.withResource(fromNickname);

        mucModule.publishMessage(fromRoomJid, fromAddress, fromNickname, messages);

        receive(fromAddress, fromRoomJid, messages);

        if (sendToCluster) {
            service.getServer().getCluster().sendMessageToAll(new RoomBroadcastTask(this, service.getSubDomain(),
                    roomId, fromAddress, fromRoomJid, fromNickname, messages), false);
        }
    }

    public void receive(KixmppJid fromJid, KixmppJid fromRoomJid, String... messages) {
        MucRoomEventHandler handler = service.getServer().getMucRoomEventHandler();
        if (handler != null) {
            handler.handleMessage(this, fromJid, fromRoomJid, messages);
        }
    }

    /**
     * Sends an direct invitation to a user.  Note: The smack client does not recognize this as a valid room invite.
     */
    public void sendDirectInvite(KixmppJid from, Channel userChannelToInvite, String reason) {
        Element message = new Element("message");
        message.setAttribute("to", userChannelToInvite.attr(BindKixmppServerModule.JID).get().getFullJid());
        if (from != null) {
            message.setAttribute("from", from.getFullJid());
        }

        Element x = new Element("x", Namespace.getNamespace("jabber:x:conference"));
        x.setAttribute("jid", roomJid.getFullJid());
        if (reason != null) {
            x.setAttribute("reason", reason);
        }

        message.addContent(x);

        userChannelToInvite.writeAndFlush(message);
    }

    /**
     * Sends a mediated invitation to a user.
     */
    public void sendMediatedInvite(KixmppJid from, Channel userChannelToInvite, String reason) {

        Element invite = new Element("invite");
        invite.setAttribute("from", from.getFullJid());
        if (reason != null) {
            Element el = new Element("reason");
            el.setText(reason);
            invite.addContent(el);
        }

        Element x = new Element("x", Namespace.getNamespace("http://jabber.org/protocol/muc#user"));
        x.addContent(invite);

        Element message = new Element("message");
        message.setAttribute("to", userChannelToInvite.attr(BindKixmppServerModule.JID).get().getFullJid());
        message.setAttribute("from", roomJid.getFullJid());
        message.addContent(x);

        userChannelToInvite.writeAndFlush(message);
    }

    public List<User> getUsers() {
        return Lists.newArrayList(usersByNickname.values());
    }

    public User getUser(String nickname) {
        return usersByNickname.get(nickname);
    }

    public User getUser(KixmppJid jid) {
        String nickname = nicknamesByBareJid.get(jid.withoutResource());
        if (nickname != null) {
            return usersByNickname.get(nickname);
        }
        return null;
    }

    private class CloseChannelListener implements GenericFutureListener<Future<? super Void>> {
        private final Client client;

        /**
         * @param client
         */
        public CloseChannelListener(Client client) {
            this.client = client;
        }

        public void operationComplete(Future<? super Void> future) throws Exception {
            leave(client);
        }
    }

    private void removeDisconnectedUser(User user) {
        if (user.getClientCount() == 0) {
            MucRole role = jidRoles.get(user.getBareJid());

            if (settings.isPresenceEnabled()) {
                broadcastPresence(roomJid.withResource(user.getNickname()), role, "unavailable");
            }
            this.usersByNickname.remove(user.getNickname());
            this.jidAffiliations.remove(user.getBareJid());
            this.jidRoles.remove(user.getBareJid());
            MucRoomEventHandler handler = service.getServer().getMucRoomEventHandler();
            if (handler != null) {
                handler.userRemoved(this, user);
            }
        }
        if (usersByNickname.isEmpty()) {
            this.service.removeRoom(roomId);
        }
    }

    /**
     * Represents a single user within a room. A User owns a unique nickname
     * within the room, but may have multiple Connections (1 per full JID)
     */
    public class User {
        private String nickname;
        private KixmppJid bareJid;
        private Map<Channel, Client> clientsByChannel = new HashMap<>();
        private Map<KixmppJid, Client> clientsByAddress = new HashMap<>();

        public User(String nickname, KixmppJid bareJid) {
            this.nickname = nickname;
            this.bareJid = bareJid.withoutResource();
        }

        public Client addClient(Client client) {
            Preconditions.checkNotNull(client.getAddress().getResource());

            clientsByChannel.put(client.getChannel(), client);
            clientsByAddress.put(client.getAddress(), client);

            return client;
        }

        public Client getClient(Channel channel) {
            return clientsByChannel.get(channel);
        }

        public Client getClient(KixmppJid address) {
            return clientsByAddress.get(address);
        }

        public KixmppJid getBareJid() {
            return bareJid;
        }

        public void receivePresence(KixmppJid fromRoomJid, MucRole role, String type) {
            for (Client client : clientsByAddress.values()) {
                Element presence = createPresence(fromRoomJid, client.getAddress(), role, type);
                client.getChannel().writeAndFlush(presence);
            }
        }

        public Collection<Client> getConnections() {
            return clientsByAddress.values();
        }

        public void removeClient(Client client) {
            clientsByAddress.remove(client.getAddress());
            clientsByChannel.remove(client.getChannel());
        }

        public String getNickname() {
            return nickname;
        }

        public int getClientCount() {
            return clientsByAddress.size();
        }

        public void removeClients() {
            clientsByAddress.clear();
            clientsByChannel.clear();
        }

    }

    /**
     * Represents single connected occupant in the room.
     */
    public class Client {
        private KixmppJid address;
        private String nickname;
        private Channel channel;

        public Client(KixmppJid address, String nickname, Channel channel) {
            Preconditions.checkNotNull(address.getResource());

            this.address = address;
            this.nickname = nickname;
            this.channel = channel;
        }

        public String getNickname() {
            return nickname;
        }

        public KixmppJid getAddress() {
            return address;
        }

        public Channel getChannel() {
            return channel;
        }
    }
}