ChatServerApp.java

/***************************************************************************
   Copyright 2014 Emily Estes

   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.
***************************************************************************/
package net.metanotion.web.examples;


import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentHashMap;
import javax.inject.Named;

import net.metanotion.authident.CredentialedUserToken;
import net.metanotion.authident.SimpleRealm;
import net.metanotion.authident.UserToken;

import net.metanotion.formsauth.AuthAPI;
import net.metanotion.formsauth.AuthFactory;
import net.metanotion.formsauth.AuthFsm;
import net.metanotion.formsauth.AuthStates;

import net.metanotion.io.JavaFileSystem;

import net.metanotion.scripting.ObjectServer;

import net.metanotion.simpletemplate.MountPointResources;
import net.metanotion.simpletemplate.ResourceDispatcher;
import net.metanotion.simpletemplate.ResourceFactory;
import net.metanotion.simpletemplate.StaticResources;

import net.metanotion.util.Extends;
import net.metanotion.util.Json;
import net.metanotion.util.Service;
import net.metanotion.util.StateMachine;
import net.metanotion.util.Unknown;
import net.metanotion.util.UnknownMagic;

import net.metanotion.web.HttpGet;
import net.metanotion.web.HttpPost;
import net.metanotion.web.RequestObject;
import net.metanotion.web.Session;
import net.metanotion.web.SessionFactory;
import net.metanotion.web.concrete.HttpException;
import net.metanotion.web.concrete.HttpUtil;
import net.metanotion.web.concrete.JsonUtil;
import net.metanotion.web.concrete.ObjectPrefixDispatcher;
import net.metanotion.web.concrete.URIPrefixDispatcher;
import net.metanotion.web.concrete.WebInterfaceDispatcher;
import net.metanotion.web.servlets.ServerUtil;

/** This demonstrates an incredibly simple chat server app with multiple chat rooms. This is a demonstration only, and
if you put this into production somewhere, you're basically insane.
*/
public final class ChatServerApp implements SessionFactory<RequestObject>, AuthStates<ChatServerApp, Session<ChatServerApp>> {
	private static final int ARG_HTML = 0;
	private static final int ARG_JS_LIBS = 1;

	private static final int SERVER_PORT = 8080;
	private static final String AUTH_PREFIX = "/auth/";
	private static final String CHAT_PREFIX = "/chat/";
	private static final String MANAGE_PREFIX = "/manage/";
	private static final int MAX_SCROLLBACK = 10;

	public static final String ROOM = "room";
	public static final String USER = "user";
	public static final String MESSAGE = "message";

	/** This is the start up for the chat server app.
		@param args The command line arguments, in order:
			<ul>
				<li>folder for the HTML</li>
				<li>folder for the framework JS libraries.</li>
			</ul>
	*/
	public static void main(final String[] args) {
		try {
			final JavaFileSystem files = new JavaFileSystem(args[ARG_HTML]);
			final JavaFileSystem jsContent = new JavaFileSystem(args[ARG_JS_LIBS]);
			final ResourceFactory resources =
				new StaticResources(files, new MountPointResources(new StaticResources(jsContent), "/js"));

			final AuthAPI authApi = AuthFactory.instance(new SimpleRealm(), AUTH_PREFIX);

			final URIPrefixDispatcher dispatcher = new URIPrefixDispatcher()
				.addDispatcher(MANAGE_PREFIX, new WebInterfaceDispatcher<>(Room.class))
				.addDispatcher(CHAT_PREFIX, new ObjectPrefixDispatcher<Unknown>(new WebInterfaceDispatcher<>(Chat.class), ROOM))
				.addDispatcher(AUTH_PREFIX, AuthFactory.dispatcher())
				.addDispatcher("", new ResourceDispatcher());

			ServerUtil.launchJettyServer(SERVER_PORT, dispatcher, new ChatServerApp(authApi, resources));
		} catch (final Exception e) {
			e.printStackTrace();
		}
	}

	@Service public final AuthAPI authAPI;
	@Service public final ResourceFactory resources;
	@Service public final ObjectServer os = null;
	public final ChatServer chat = new ChatServer();

	private final AuthFsm<ChatServerApp, Session<ChatServerApp>> factory = new AuthFsm<>(this);

	/** Create an instance of the chat server application.
		@param authAPI the Authentication API service.
		@param resources HTML and JS resources.
	*/
	public ChatServerApp(final AuthAPI authAPI, final ResourceFactory resources) {
		this.authAPI = authAPI;
		this.resources = resources;
	}

	/** The API for creating and listing chat rooms. */
	public interface Room {
		/** Retrieve the JSON API describing the methods of this interface.
			@return The JSON API.
		*/
		@HttpGet @Json public Object api();

		/** Retrieve a list of the JSON API's for the chat rooms on this server.
			@return A list of JSON API's in the following format: { rooms: [ {...}, ... ] }
		*/
		@HttpGet @Json public RoomList list();

		/** Create a new chat room.
			@param room The name of the chat room.
			@return The JSON API for the chat room.
		*/
		@HttpPost @Json public Object create(@Named(ROOM) String room);
	}

	/** An interface for interacting with a chat room. */
	public interface Chat {
		/** Retrieve the scrollback record of a chat room.
			@param room The chat room to retrieve.
			@return A JSON object listing all the messages in the scrollback.
		*/
		@HttpGet @Json public Scrollback messageList(@Named(ROOM) String room);

		/** Send a message to a chat room.
			@param ut The user who sent the message.
			@param room The room to send the message to.
			@param message The text of the message to send.
			@return A JSON object describing the message delivered.
		*/
		@HttpPost @Json public ChatMessage send(@Named(USER) @Service UserToken ut,
			@Named(ROOM) String room,
			@Named(MESSAGE) String message);
	}

	/** A list of chat rooms. */
	public static final class RoomList {
		public final Iterable<Object> rooms;
		/** Create a list of chat rooms.
			@param rooms The list of JSON API's for the individual chat rooms.
		*/
		public RoomList(Iterable<Object> rooms) { this.rooms = rooms; }
	}

	/** An individual message in a chat room. */
	public static final class ChatMessage {
		public final String message;
		public final String user;
		public final long id;

		/** An individual message in a chat room.
			@param message The text of the message.
			@param user The user who sent the message.
			@param id The unique id for the message.
		*/
		public ChatMessage(final String message, final String user, final long id) {
			this.message = message;
			this.user = user;
			this.id = id;
		}
	}

	/** This class represents the scroll back buffer for a chat room. */
	public static final class Scrollback {
		public final LinkedList<ChatMessage> messages = new LinkedList<>();
		private long messageCounter = 0;

		/** Add a new message to this chat room.
			@param user The user who sent the message.
			@param message The text of the message the user sent.
			@return A ChatMessage representing the message.
		*/
		public synchronized ChatMessage add(final String user, final String message) {
			final ChatMessage m = new ChatMessage(message, user, messageCounter++);
			messages.add(m);
			while(messages.size() > MAX_SCROLLBACK) {
				messages.removeFirst();
			}
			return m;
		}
	}

	/** This class represents the logic for manipulating chat rooms. */
	public static final class ChatServer implements Chat, Room {
		private final ConcurrentHashMap<String,Scrollback> rooms = new ConcurrentHashMap<>();

		private Object endpoint(final String room) {
			final HashMap<String,Object> v = new HashMap<>();
			v.put(ROOM, room);
			return JsonUtil.makeWebApi(Chat.class, CHAT_PREFIX + room + "/", v);
		}

		// Room methods
		@Override public Object api() { return JsonUtil.makeWebApi(Room.class, MANAGE_PREFIX); }

		@Override public RoomList list() {
			final LinkedList<String> keys = new LinkedList<>(rooms.keySet());
			Collections.sort(keys);
			final LinkedList<Object> list = new LinkedList<>();
			for(final String key: keys) {
				list.add(endpoint(key));
			}
			return new RoomList(list);
		}

		@Override public Object create(final String room) {
			rooms.putIfAbsent(room, new Scrollback());
			return endpoint(room);
		}

		private static final HttpException NOT_FOUND = new HttpException(HttpUtil.new404(""));

		// Chat methods
		@Override public Scrollback messageList(final String room) {
			try {
				final String r = URLDecoder.decode(room, "UTF-8");
				final Scrollback ret = rooms.get(r);
				if(ret == null) { throw NOT_FOUND; }
				return ret;
			} catch (UnsupportedEncodingException uee) {
				// this should NEVER happen. UTF-8 is mandated as part of the JVM.
				throw new RuntimeException(uee);
			}
		}

		@Override public ChatMessage send(final UserToken ut, final String room, final String message) {
			return messageList(room).add(((CredentialedUserToken) ut).getCredential().toString(), message);
		}
	}

	// SessionFactory methods
	@Override public Unknown newSession(final RequestObject ro) { return factory.newSession(this); }

	// AuthStates methods
	@Override public Session<ChatServerApp> login(final UserToken user, final Session<ChatServerApp> currentState) {
		return new AuthenticatedSession(currentState.appInstance(), user);
	}

	@Override public Session<ChatServerApp> logout(final Session<ChatServerApp> currentState) {
		return new AnonymousSession(currentState.appInstance());
	}

	@Override public Session<ChatServerApp> newSession(final ChatServerApp app, final StateMachine sm) {
		return new AnonymousSession(this);
	}

	/** A session for users that aren't authenticated. */
	public static final class AnonymousSession implements Session<ChatServerApp> {
		private static final UnknownMagic u = new UnknownMagic(AnonymousSession.class);
		@Extends public final ChatServerApp app;
		@Service public final UserToken ut = null;

		/** Create a new anonymous session.
			@param app The chat server app instance.
		*/
		public AnonymousSession(final ChatServerApp app) { this.app = app; }

		@Override public ChatServerApp appInstance() { return this.app; }
		// Unknown
		@Override public <I> I lookupInterface(final Class<I> theInterface) { return u.lookupInterface(theInterface, this); }
	}

	/** A session for an authenticated user. */
	public static final class AuthenticatedSession implements Session<ChatServerApp> {
		private static final UnknownMagic u = new UnknownMagic(AuthenticatedSession.class);
		@Extends public final ChatServerApp app;
		@Service public final UserToken ut;
		@Service public final ChatServer chat;

		/** Create a new authenticated user session.
			@param app The chat server app instance.
			@param ut The user token for this session's user.
		*/
		public AuthenticatedSession(final ChatServerApp app, final UserToken ut) {
			this.app = app;
			this.ut = ut;
			this.chat = app.chat;
		}

		@Override public ChatServerApp appInstance() { return this.app; }
		// Unknown
		@Override public <I> I lookupInterface(final Class<I> theInterface) { return u.lookupInterface(theInterface, this); }
	}
}