package server;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Map;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
/**
* An HTTP server that lets clients chat from a web browser.
*
* @author akishore
*
*/
public class ApplicationServer extends
NanoHTTPD {
//
Constants
private static final int
DEFAULT_PORT = 4000;
private static final String
FILE_PATH = "src/client/";
private static final String
HTML_FILE = "index.html";
private static final String
CID_REGEXP = "\\/\\*CLIENT_ID\\*\\/";
private static final String[] FILES = {
"constants.js", "controller.js",
"model.js", "prototype.js",
"scrollpane.js", "style.css",
"view.js"
};
private static final String
BASE_URI = "/";
private static final String
REFRESH_URI = "/refresh";
private static final String
SEND_URI = "/send";
private static final String
ID_ARG = "id";
private static final String
MSG_ARG = "msg";
//
Utilities
private static final Random
random = new Random();
// Fields
private final int
port;
/**
* Maps client ID to a queue
of messages to be sent *to* that client.
* So when the client
refreshes, these messages are sent to him/her.
* The ConcurrentMap
interface provides additional convenience methods,
* e.g. putIfAbsent(), to
handle concurrency even better.
*/
private final ConcurrentMap<String, Queue<String>>
messages;
//
...any additional fields you need
// Constructors
public
ApplicationServer() throws IOException {
this(DEFAULT_PORT);
}
public
ApplicationServer(int port) throws IOException
{
super(port);
this.port =
port;
// important: using a ConcurrentHashMap
because multiple threads
// (from concurrent HTTP sessions) may be accessing map
this.messages =
new ConcurrentHashMap<String,
Queue<String>>();
// if the server needs to be configured
somehow...
configure();
}
//
Public methods
public
int getPort() {
return port;
}
/**
* This is the sole
NanoHTTPD method we need to override. Given a URI
* and parameters (key-value
pairs), we'll return the appropriate HTTP
* response.
*/
@Override
public
Response serve(String uri, Map<String,
String> params) {
assert uri !=
null;
assert params
!= null;
// serve the response based on the uri
if (uri.equals(BASE_URI)) {
return serveBase(params);
} else if
(uri.equals(REFRESH_URI)) {
return serveRefresh(params);
} else if
(uri.equals(SEND_URI)) {
return serveSend(params);
} else if
(isKnownFile(uri.substring(1))) { // strip leading slash
return serveFile(uri);
}
System.err.println("Received bad uri: "
+ uri);
return badResponse();
}
//
Serve methods
/**
* Generates a random ID for
this client and serves the index.html
* page to the client.
*/
private Response
serveBase(Map<String, String> params) {
// parameters are ignored
try {
// try to load the HTML page, may throw an
IOException
String html = loadFile(FILE_PATH +
HTML_FILE);
// make a random ID for this user
int id =
random.nextInt(Integer.MAX_VALUE);
String strId = Integer.toString(id);
System.out.println("New user! " +
strId);
// make a queue for this user to receive
messages.
// important: using a ConcurrentLinkedQueue in case multiple
// threads try to get/put messages at the same time.
messages.putIfAbsent(strId, new ConcurrentLinkedQueue<String>());
// set the HTML page to have this ID, and
return it
html = html.replaceAll(CID_REGEXP + ".*" + CID_REGEXP,
strId);
return goodResponse(html);
} catch
(IOException
e) {
System.err.println("The HTML page failed to
load:");
e.printStackTrace(System.err);
return badResponse();
}
}
/**
* Given an ID argument in
the parameters, serves the messages that
* have been queued for the
client with the given ID. The messages are
* separated by newlines
(\n). If the client has no queued messages,
* serves the empty string.
*/
private Response
serveRefresh(Map<String, String> params) {
try {
checkParam(params, ID_ARG);
} catch
(IllegalArgumentException e)
{
System.err.println("Bad refresh request:
" + e.getMessage());
return badResponse();
}
return goodResponse(fetchMessages(params.get(ID_ARG)));
}
/**
* Given an ID argument and
a message argument in the parameters, sends
* the given message
appropriately. Always serves an empty string.
*/
private Response
serveSend(Map<String, String> params) {
try {
checkParam(params, ID_ARG);
checkParam(params, MSG_ARG);
} catch
(IllegalArgumentException e)
{
System.err.println("Bad send request: "
+ e.getMessage());
return badResponse();
}
sendMessage(params.get(ID_ARG), params.get(MSG_ARG));
return goodResponse();
}
/**
* Serves the file at the given
URI, which represents a filename. The
* filename must have an extension
(i.e. must have a dot).
*/
private Response
serveFile(String uri) {
assert uri.contains("."); // must be a file with an
extension
// figure out the MIME type based on the file extension
String mime = "text/plain";
// default, if unknown extension
String ext = uri.substring(uri.lastIndexOf('.'));
if (ext.equals(".js")) {
mime = "text/javascript";
} else if
(ext.equals(".css")) {
mime = "text/css";
}
try {
return new Response(HTTP_OK, mime, loadFile(FILE_PATH + uri));
} catch
(IOException
e) {
System.err.println("Unknown file to serve:
" + uri);
return badResponse();
}
}
//
Helper methods
/**
* Returns the contents of
the given file, or throws an IOException if
* the file could not be
read for any reason. Note that the filename
* should be relative to the
runtime classpath (in Eclipse, this means
* the project directory),
not to this class's file path.
*/
private static String
loadFile(String
filename) throws
IOException {
BufferedReader file = new BufferedReader(new FileReader(filename));
StringBuilder text = new StringBuilder();
String line = file.readLine();
while (line != null) {
text.append(line);
text.append("\n");
line = file.readLine();
}
return text.toString();
}
/**
* Returns the messages
currently queued for the client with the given
* ID, and removes those
messages from the queue. If the ID is not
* found, or if there are no
messages queued for this client, returns
* an empty string. The
messages are separated by a newline (\n).
*/
private String
fetchMessages(String id) {
// using a StringBuilder is much faster
than repeatedly using the
// string concatenation (+) operator
StringBuilder message = new StringBuilder();
Queue<String> queue = messages.get(id);
if (queue != null) {
while (!queue.isEmpty()) {
message.append(queue.poll()); // poll() removes the message too
message.append("\n");
}
}
return message.toString();
}
/**
* Sends the given message
from the client with the given ID to every
* other client on the server.
*/
private void sendMessage(String id, String message)
{
// add the message to every queue in our
map
for (Queue<String> queue :
messages.values()) {
queue.add(id + ": " +
message);
}
// TODO Task 2: Modify this method so that
sending a message no
// longer broadcasts it to every other client, and instead
// broadcasts Friendly's response to only this client. Make sure
// to still broadcast the original message to the client who sent
// it, so that they may see their own message.
}
/**
* Configures the
application server on startup.
*/
private void configure() {
// TODO Task 3: Modify this method to
initialize your Friendly chat
// server with the responses specified in your XML file. You might
// like to use the XmlConfiguration stub, shown below.
//
try {
//
// it can be better to load files via class resources instead
//
// of directly; just note that the file name is then relative
//
// to this class's Java file, not the project folder.
//
//
// ... = new XmlConfiguration(
//
//
ApplicationServer.class.getResourceAsStream(/*XML_FILE*/));
//
// ...
//
//
} catch (ParserConfigurationException e) {
//
System.err.println("Native parser error:");
//
e.printStackTrace(System.err);
//
} catch (SAXException e) {
//
System.err.println("XML file failed to parse:");
//
e.printStackTrace(System.err);
//
} catch (IOException e) {
//
System.err.println("XML file not found:");
//
e.printStackTrace(System.err);
//
}
}
private void checkParam(Map<String, String> params, String
param) {
if (!params.containsKey(param)) {
throw new IllegalArgumentException("missing param: "
+ param);
} else if
(params.get(param).equals("")) {
throw new IllegalArgumentException("missing value for "
+ param);
}
}
/**
* Returns true iff the
given filename represents a known server file.
*/
private boolean isKnownFile(String filename) {
for (String known :
FILES) {
if (known.equals(filename)) {
return true;
}
}
return false;
}
//
Main method
/**
* Attempts to run the
application server, either on the port given as
* the first argument, or
the default port. The server will run until
* you press enter or until
you forcefully terminate the program.
*/
public
static void main(String[] args) throws IOException
{
ApplicationServer server = null;
if (args.length >= 1) {
// try to see if first argument is a valid
port number
try {
server = new
ApplicationServer(Integer.parseInt(args[0]));
} catch
(NumberFormatException e)
{
System.err.println("Invalid port number:
" + args[0]);
} catch
(IOException
e) {
System.err.println("Could not listen on
port " + args[0]);
}
} else {
// no arguments given, use default port
try {
server = new
ApplicationServer();
} catch
(IOException
e) {
System.err.println("Could not listen on
default port");
}
}
if (server != null) {
System.out.println("Server running on port
" + server.getPort() + "...");
System.out.println("Press enter to end.");
System.in.read(); // may throw an
IOException here if user quits
}
}
}