com.kolich.pusachat.spring.controllers.AbstractPusaChatController.java Source code

Java tutorial

Introduction

Here is the source code for com.kolich.pusachat.spring.controllers.AbstractPusaChatController.java

Source

/**
 * Copyright (c) 2013 Mark S. Kolich
 * http://mark.koli.ch
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package com.kolich.pusachat.spring.controllers;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.kolich.pusachat.entities.ChatRoom.isValidRoomName;
import static com.kolich.pusachat.util.MessageToHtml.makeHyperlinks;
import static java.util.UUID.fromString;
import static java.util.UUID.randomUUID;
import static java.util.concurrent.Executors.newCachedThreadPool;
import static org.apache.commons.lang3.StringEscapeUtils.escapeHtml4;

import java.util.UUID;
import java.util.concurrent.ExecutorService;

import org.slf4j.Logger;

import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.kolich.common.util.secure.KolichStringSigner;
import com.kolich.pusachat.entities.ChatLog;
import com.kolich.pusachat.entities.ChatRoom;
import com.kolich.pusachat.entities.PusaChatSession;
import com.kolich.pusachat.entities.events.ClientJoined;
import com.kolich.pusachat.entities.events.ClientStatus;
import com.kolich.pusachat.entities.events.Delete;
import com.kolich.pusachat.entities.events.Message;
import com.kolich.pusachat.entities.events.PusaChatEvent;
import com.kolich.pusachat.entities.events.Typing;
import com.kolich.pusachat.exceptions.BadChatTokenException;
import com.kolich.pusachat.exceptions.InvalidRoomNameException;
import com.kolich.pusachat.spring.beans.ChatRooms;

public abstract class AbstractPusaChatController {

    /**
     * The default global delimiter that's used to logically separate
     * a room {@link UUID} and a client {@link UUID} in a signed token.
     */
    private static final String TOKEN_DELIMITER = "_";

    protected final Logger logger_;

    protected final KolichStringSigner signer_;
    protected final ChatRooms rooms_;

    /**
     * A thread pool is used to "post" events to the queues of clients.
     * This is so that the caller does not have to wait on the infrastructre
     * posting events, which may or may not be a slow/costly operation
     * depending on how busy the room is.
     */
    private static final ExecutorService postEventPool__ = newCachedThreadPool(new ThreadFactoryBuilder()
            .setDaemon(true).setNameFormat("post-event-pool-%s").setPriority(Thread.MAX_PRIORITY).build());

    protected AbstractPusaChatController(Logger logger, KolichStringSigner signer, ChatRooms rooms) {
        logger_ = logger;
        signer_ = signer;
        rooms_ = rooms;
    }

    protected ChatRoom getRoom(final UUID roomId) {
        return rooms_.getRoom(roomId);
    }

    protected ChatRoom getRoomFromKey(final String key, final boolean create) {
        // Validate the room name; must be properly formed.
        if (!isValidRoomName(key)) {
            throw new InvalidRoomNameException("Invalid room name: " + key);
        }
        return rooms_.getRoomFromKey(key, create);
    }

    protected PusaChatSession getSession(final String token) {
        return getRoom(getRoomIdFromToken(token)).getSessionByClientId(getClientIdFromToken(token))
                // Update the last accessed time too.
                .wasAccessed();
    }

    protected void postEvent(final ChatRoom room, final PusaChatEvent event) {
        // Post the event to the queue in a separate thread.
        postEventPool__.execute(new Runnable() {
            @Override
            public void run() {
                // Post the event to all clients in the room.
                room.postEvent(event);
                // Only need to persist events of type "Message" to log
                // chat messages -- any other event is dropped on the floor.
                if (event instanceof Message) {
                    room.getChatLog().addMessage((Message) event);
                } else if (event instanceof Delete) {
                    room.getChatLog().deleteMessage((Delete) event);
                }
            }
        });
    }

    protected void postEvent(final UUID roomId, final PusaChatEvent event) {
        postEvent(getRoom(roomId), event);
    }

    protected PusaChatSession registerClient(final UUID roomId) {
        final ChatRoom room = getRoom(roomId);
        final UUID clientId = randomUUID();
        final PusaChatSession session = room.registerClient(
                // Register the randomly generated new client UUID.
                clientId,
                // Also pass in the token which is a digitally signed
                // String containing the room UUID and client UUID. This
                // is an unguessable ("secure") signed unique identifier.
                createToken(room.getId(), clientId));
        // Post an event that a new client with the given ID joined
        // the chat room.
        postEvent(room, new ClientJoined(clientId));
        // Also post an event containing the new status of the room
        // listing both active and inactive clients.
        postEvent(room, room.getClientStatus());
        return session;
    }

    protected Message postMessage(final UUID roomId, final UUID clientId, final String message) {
        // Build a new message.
        final Message m = new Message(clientId.toString(), makeHyperlinks(escapeHtml4(message)));
        // Post the event to the room so it's picked up by any waiting clients.
        postEvent(roomId, m);
        return m;
    }

    protected Delete postDeleteMessage(final UUID roomId, final UUID clientId, final String messageId) {
        checkNotNull(messageId, "Oops, message ID to delete cannot be null!");
        final Delete delete = new Delete(fromString(messageId));
        // Post the event to the room so it's picked up by any waiting clients.
        postEvent(roomId, delete);
        return delete;
    }

    protected Typing setTypingStatus(final UUID roomId, final UUID clientId, final boolean isTyping) {
        final ChatRoom room = getRoom(roomId);
        final Typing typing = room.updateTyping(clientId, isTyping);
        postEvent(room, typing);
        return typing;
    }

    protected ClientStatus setInactivityStatus(final UUID roomId, final UUID clientId, final boolean isActive) {
        final ChatRoom room = getRoom(roomId);
        final ClientStatus status = room.updateInactivity(clientId, isActive);
        postEvent(room, status);
        return status;
    }

    protected ChatLog getChatLog(final UUID roomId) {
        return getRoom(roomId).getChatLog();
    }

    protected String createToken(final UUID roomId, final UUID clientId) {
        checkNotNull(roomId, "Oops, room UUID cannot be null!");
        checkNotNull(clientId, "Oops, client UUID cannot be null!");
        // Probably don't need to call toString() on the UUID's but
        // I'm being pedantic.
        return signer_.sign(roomId.toString() + TOKEN_DELIMITER + clientId.toString());
    }

    protected UUID getRoomIdFromToken(final String token) {
        try {
            return fromString(getTokensFromPayload(signer_.isValid(token))[0]);
        } catch (Exception e) {
            throw new BadChatTokenException("Failed to extract room " + "UUID from token: " + token, e);
        }
    }

    protected UUID getClientIdFromToken(final String token) {
        try {
            return fromString(getTokensFromPayload(signer_.isValid(token))[1]);
        } catch (Exception e) {
            throw new BadChatTokenException("Failed to extract client " + "UUID from token: " + token, e);
        }
    }

    private static final String[] getTokensFromPayload(final String payload) {
        checkNotNull(payload, "Oops, token payload cannot be null!");
        return payload.split(TOKEN_DELIMITER, 2 /* important */);
    }

}