diff --git a/pom.xml b/pom.xml index 9d27576..67eac0f 100755 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 net.jonathangiles.tools teenyhttpd - 1.0.5 + 1.0.6 TeenyHttpd TeenyHttpd is an extremely basic HTTP server. @@ -83,7 +83,7 @@ true - net.jonathangiles.tools.teenyhttpd.Main + net.jonathangiles.tools.teenyhttpd.TeenyHttpd @@ -92,6 +92,9 @@ org.apache.maven.plugins maven-javadoc-plugin 3.6.0 + + *.implementation.* + attach-javadoc diff --git a/readme.md b/readme.md index 832334f..67fcb4f 100755 --- a/readme.md +++ b/readme.md @@ -116,7 +116,7 @@ the following code will serve a response of `User ID: 123` when a GET request is ```java server.addGetRoute("/user/:id/details", request -> { String id = request.getPathParams().get("id"); - return new StringResponse(StatusCode.OK, "User ID: " + id); + return Response.create(StatusCode.OK, "User ID: " + id); }); ``` @@ -139,6 +139,71 @@ test = true foo = bar ``` +## Server-Sent Events + +TeenyHttpd supports Server-Sent Events (SSE). To use this feature, you need to use the `addServerSentEventRoute` method, +with a `ServerSentEventHandler`. For example, the following code will send a message to the client every second: + +```java +final int PORT = 80; +TeenyHttpd server = new TeenyHttpd(PORT); +server.addServerSentEventRoute("/events", ServerSentEventHandler.create(sse -> new Thread(() -> { + int i = 0; + while (sse.hasActiveConnections()) { + sse.sendMessage(new ServerSentEventMessage("Message " + i++, "counter")); + threadSleep(1000); + } +}).start())); +server.start(); +``` + +If more than one user connects to the same /events topic, they will share the same state, each getting the same value +for `i` at the same time. If you want to customise the response per user (for example, based on a path parameter or +query parameter), you can use the message generator feature: + +```java +ServerSentEventHandler sse = ServerSentEventHandler.create((ServerSentEventHandler _sse) -> { + // start a thread and send messages to the client(s) + new Thread(() -> { + // all clients share the same integer value, but they get a custom message based + // on the path parameter for :username + AtomicInteger i = new AtomicInteger(0); + + while (_sse.hasActiveConnections()) { + _sse.sendMessage(client -> { + String username = client.getPathParams().get("username"); + return new ServerSentEventMessage("Hello " + username + " - " + i, "counter"); + }); + i.incrementAndGet(); + threadSleep(1000); + } + }).start(); +}); +server.addServerSentEventRoute("/sse/:username", sse); +``` + +The above samples assume that you start a thread when there are active connections, to send messages to connected +clients at a regular interval. Another approach is to just have a ServerSentEventHandler that sends messages to +connected clients when a message is received. For example, the following code will send a message to all clients +that are connected to the `/messages` topic, when a message is posted to `/message`: + +```java +final int PORT = 80; +TeenyHttpd server = new TeenyHttpd(PORT); +ServerSentEventHandler chatMessagesEventHandler = ServerSentEventHandler.create(); +server.addServerSentEventRoute("/messages", chatMessagesEventHandler); +server.addRoute(Method.POST, "/message", request -> { + String message = request.getQueryParams().get("message"); + if (message != null && !message.isEmpty()) { + chatMessagesEventHandler.sendMessage(message); + } + return StatusCode.OK.asResponse(); +}); +``` + +For a complete example, check out the [ChatServer](src/test/java/net/jonathangiles/tools/teenyhttpd/ChatServer.java) +demo application, that demonstrates how to use Server-Sent Events to create a simple chat server. + ### Stopping TeenyHttpd You stop a running instance as follows: diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index e70e174..2a3d097 100755 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,5 +1,4 @@ module net.jonathangiles.tools.teenyhttpd { exports net.jonathangiles.tools.teenyhttpd; - exports net.jonathangiles.tools.teenyhttpd.request; - exports net.jonathangiles.tools.teenyhttpd.response; + exports net.jonathangiles.tools.teenyhttpd.model; } \ No newline at end of file diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/TeenyHttpd.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/TeenyHttpd.java index d07a5e7..4a42a0d 100755 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/TeenyHttpd.java +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/TeenyHttpd.java @@ -1,13 +1,18 @@ package net.jonathangiles.tools.teenyhttpd; -import net.jonathangiles.tools.teenyhttpd.request.Header; -import net.jonathangiles.tools.teenyhttpd.request.Method; -import net.jonathangiles.tools.teenyhttpd.request.QueryParams; -import net.jonathangiles.tools.teenyhttpd.request.Request; -import net.jonathangiles.tools.teenyhttpd.response.FileResponse; -import net.jonathangiles.tools.teenyhttpd.response.Response; -import net.jonathangiles.tools.teenyhttpd.response.StatusCode; -import net.jonathangiles.tools.teenyhttpd.response.StringResponse; +import net.jonathangiles.tools.teenyhttpd.implementation.Main; +import net.jonathangiles.tools.teenyhttpd.model.ContentType; +import net.jonathangiles.tools.teenyhttpd.model.Header; +import net.jonathangiles.tools.teenyhttpd.model.Headers; +import net.jonathangiles.tools.teenyhttpd.model.ServerSentEventHandler; +import net.jonathangiles.tools.teenyhttpd.implementation.ServerSentEventRequest; +import net.jonathangiles.tools.teenyhttpd.model.Method; +import net.jonathangiles.tools.teenyhttpd.model.QueryParams; +import net.jonathangiles.tools.teenyhttpd.model.Request; +import net.jonathangiles.tools.teenyhttpd.implementation.FileResponse; +import net.jonathangiles.tools.teenyhttpd.model.Response; +import net.jonathangiles.tools.teenyhttpd.model.StatusCode; +import net.jonathangiles.tools.teenyhttpd.implementation.StringResponse; import java.io.BufferedOutputStream; import java.io.BufferedReader; @@ -60,7 +65,14 @@ public class TeenyHttpd { private CountDownLatch startLatch; - private final Map>> routes = new HashMap<>(); + private final Map> routes = new HashMap<>(); + + /** + * Starts a new server instance. + */ + public static void main(String... args) { + Main.main(args); + } /** * Creates a single-threaded server that will work on the given port, although the server does not start until @@ -115,8 +127,26 @@ public void addFileRoute(String path, final File webroot) { }); } + public void addServerSentEventRoute(String path, ServerSentEventHandler sse) { + Route sseRoute = new Route(Method.GET, new RequestPath(path), request -> { + Response response = StatusCode.OK.asResponse(); + response.setHeader(Headers.CONTENT_TYPE.asHeader(ContentType.EVENT_STREAM.getHeaderValue())); + response.setHeader(Headers.CACHE_CONTROL.asHeader("no-cache")); + response.setHeader(Headers.CONNECTION.asHeader("keep-alive")); + response.setHeader(Headers.ACCESS_CONTROL_ALLOW_ORIGIN.asHeader("*")); + return response; + }); + sseRoute.setServerSentEventRoute(true); + sseRoute.setSseHandler(sse); + _addRoute(sseRoute); + } + private void _addRoute(final Method method, final String path, final Function handler) { - routes.computeIfAbsent(method, k -> new HashMap<>()).put(new RequestPath(path), handler); + _addRoute(new Route(method, new RequestPath(path), handler)); + } + + private void _addRoute(final Route route) { + routes.computeIfAbsent(route.method, k -> new ArrayList<>()).add(route); } /** @@ -185,7 +215,12 @@ public void stop() { } private void handleIncomingRequest(final Socket clientSocket) { - try (final BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) { + boolean isLongRunningConnection = false; + + BufferedReader in = null; + try { + in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); + // get first line of the request from the client final String input = in.readLine(); if (input == null) { @@ -200,20 +235,23 @@ private void handleIncomingRequest(final Socket clientSocket) { final Method method = Method.valueOf(parse.nextToken().toUpperCase()); // Get the map for the method from the incoming request - Map> methodRoutes = routes.get(method); + List methodRoutes = routes.get(method); // we get request-uri requested. For now we assume it is an absolute path final String requestUri = parse.nextToken(); // split it at the query param, if it exists - final Request request; + final String path; + final QueryParams queryParams; if (requestUri.contains("?")) { final String[] uriSplit = requestUri.split("\\?", 2); + path = uriSplit[0]; // create a lazily-evaluated object to represent the query parameters - request = new Request(method, uriSplit[0], new QueryParams(uriSplit[1])); + queryParams = new QueryParams(uriSplit[1]); } else { - request = new Request(method, requestUri, QueryParams.EMPTY); + path = requestUri; + queryParams = QueryParams.EMPTY; } if (methodRoutes == null) { @@ -221,8 +259,8 @@ private void handleIncomingRequest(final Socket clientSocket) { // methods. We need to check if we support it on any other methods, and if so, we need to return a // 405. If we don't support it on any other methods, we need to return a 404. boolean isSupportedOnOtherMethods = routes.values().stream() - .flatMap(m -> m.keySet().stream()) - .anyMatch(p -> p.getPath().equals(request.getPath())); + .flatMap(Collection::stream) + .anyMatch(p -> p.routePath.path.equals(path)); if (isSupportedOnOtherMethods) { // we support this path on at least one other method, so we return a 405 @@ -237,50 +275,78 @@ private void handleIncomingRequest(final Socket clientSocket) { // read (but not parse) all request headers and put them into the request. // They will be parsed on-demand. String line; + List
headers = new ArrayList<>(); while (true) { line = in.readLine(); if (line == null || line.isEmpty() || "\r\n".equals(line)) { break; } - request.addHeader(new Header(line)); + headers.add(new Header(line)); } // the request path is a full path, which may include path params within the path (e.g. ':id'), or extra path // information that comes after the root path (e.g. the root path may be '/', but we the path may be '/index.html'). // We need to determine the best route to call based on the given full path, and then pass the request to that route. - Optional>> route = methodRoutes.entrySet().stream() - .filter(entry -> { + Optional route = methodRoutes.stream() + .filter(r -> { // compare the regex path to the request path, and check if they match - return entry.getKey().getRegex().matcher(request.getPath()).matches(); + return r.routePath.getRegex().matcher(path).matches(); }).findFirst(); final Response response; + Map pathParamsMap = null; if (route.isPresent()) { // we have a route, so we call it, but first we need to parse the path params and set them in the // request - final RequestPath requestPath = route.get().getKey(); - final Matcher matcher = requestPath.getRegex().matcher(request.getPath()); + final RequestPath requestPath = route.get().routePath; + final Matcher matcher = requestPath.getRegex().matcher(path); if (matcher.matches()) { // we have a match, so we need to parse the path params and set them in the request + pathParamsMap = new HashMap<>(); final List pathParams = requestPath.getPathParams(); for (int i = 0; i < pathParams.size(); i++) { - request.addPathParam(pathParams.get(i), URLDecoder.decode(matcher.group(i + 1), "UTF-8")); + pathParamsMap.put(pathParams.get(i), URLDecoder.decode(matcher.group(i + 1), "UTF-8")); } } - response = route.get().getValue().apply(request); + + final Request request = Request.create(method, path, queryParams, headers, pathParamsMap); + + // This is where we actually call the callback that the user has provided for the given route. + // Check if the response should be a streaming type based on the request headers + if (route.get().isServerSentEventRoute()) { + // we have a request for a server-sent event, so we need to create a new ServerSentEvent instance + // and pass the request + isLongRunningConnection = true; + ServerSentEventRequest sseRequest = new ServerSentEventRequest(request, clientSocket); + + // send the standard SSE-related headers first + response = route.get().handler.apply(sseRequest); + sendResponse(sseRequest.getWriter(), null, response.getStatusCode(), response); + + // now start the SSE connection + route.get().getSseHandler().onConnect(sseRequest); + } else { + // we have a normal request, so we call the route + response = route.get().handler.apply(request); + sendResponse(clientSocket, response); + } } else { - System.out.println("No route found for " + request.getPath() + " on method " + method); - System.out.println("Available routes are:"); - methodRoutes.keySet().forEach(System.out::println); + System.out.println("No route found for " + path + " on method " + method); + System.out.println(" - Available routes are:"); + methodRoutes.forEach(rp -> System.out.println(" - " + rp.routePath)); response = StatusCode.NOT_FOUND.asResponse(); + sendResponse(clientSocket, response); } - - sendResponse(clientSocket, response); } catch (IOException e) { System.err.println("Server error 2 : " + e); } finally { try { - clientSocket.close(); + if (!isLongRunningConnection) { + if (in != null) { + in.close(); + } + clientSocket.close(); + } } catch (IOException e) { e.printStackTrace(); } @@ -298,22 +364,30 @@ private void sendResponse(Socket clientSocket, Response response) { private void sendResponse(Socket clientSocket, StatusCode statusCode, Response response) { try (final PrintWriter out = new PrintWriter(clientSocket.getOutputStream()); final BufferedOutputStream dataOut = new BufferedOutputStream(clientSocket.getOutputStream())) { + sendResponse(out, dataOut, statusCode, response); + } catch (IOException ioe) { + System.err.println("Server error when trying to serve request"); + System.err.println("Server error : " + ioe); + } + } - // write headers - out.println((statusCode == null ? response.getStatusCode() : statusCode).toString()); - out.println("Server: TeenyHttpd from JonathanGiles.net : 1.0"); - out.println("Date: " + LocalDateTime.now()); + private void sendResponse(PrintWriter out, BufferedOutputStream dataOut, StatusCode statusCode, Response response) { + try { + if (out != null) { + // write headers + out.println((statusCode == null ? response.getStatusCode() : statusCode).toString()); + out.println("Server: TeenyHttpd from JonathanGiles.net : 1.0"); + out.println("Date: " + LocalDateTime.now()); + + if (response != null) { + response.getHeaders().forEach(h -> out.println(h.toString())); + } - if (response != null) { - // FIXME shouldn't add two lots of Content-Length here - response.getHeaders().forEach(out::println); - out.println("Content-Length: " + response.getBodyLength()); + out.println(); // empty line between header and body + out.flush(); // flush character output stream buffer } - out.println(); // empty line between header and body - out.flush(); // flush character output stream buffer - - if (response != null) { + if (response != null && dataOut != null) { // write body response.writeBody(dataOut); dataOut.flush(); // flush binary output stream buffer @@ -324,6 +398,37 @@ private void sendResponse(Socket clientSocket, StatusCode statusCode, Response r } } + private static class Route { + private final Method method; + private final RequestPath routePath; + private final Function handler; + private boolean isServerSentEventRoute; + + private ServerSentEventHandler sseHandler; + + public Route(Method method, RequestPath routePath, Function handler) { + this.method = method; + this.routePath = routePath; + this.handler = handler; + } + + public boolean isServerSentEventRoute() { + return isServerSentEventRoute; + } + + public void setServerSentEventRoute(boolean isServerSentEventRoute) { + this.isServerSentEventRoute = isServerSentEventRoute; + } + + public ServerSentEventHandler getSseHandler() { + return sseHandler; + } + + public void setSseHandler(ServerSentEventHandler sseHandler) { + this.sseHandler = sseHandler; + } + } + private static class RequestPath { private final String path; private final Pattern regexPattern; diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ByteResponse.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ByteResponse.java new file mode 100755 index 0000000..2495ca6 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ByteResponse.java @@ -0,0 +1,43 @@ +package net.jonathangiles.tools.teenyhttpd.implementation; + +import net.jonathangiles.tools.teenyhttpd.model.Header; +import net.jonathangiles.tools.teenyhttpd.model.StatusCode; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +public class ByteResponse extends ResponseBase { + final byte[] body; + +// public ByteResponse(final StatusCode statusCode) { +// this(statusCode, Collections.emptyList()); +// } + +// public ByteResponse(final StatusCode statusCode, final List
headers) { +// this(statusCode, headers, null); +// } + + public ByteResponse(final StatusCode statusCode, final byte[] body) { + this(statusCode, Collections.emptyList(), body); + } + + public ByteResponse(final StatusCode statusCode, final List
headers, final byte[] body) { + super(statusCode, headers); + this.body = body; + } + + @Override + public long getBodyLength() { + return body == null ? 0 : body.length; + } + + @Override + public void writeBody(BufferedOutputStream dataOut) throws IOException { + if (body != null) { + dataOut.write(body, 0, body.length); + dataOut.flush(); + } + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/EmptyResponse.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/EmptyResponse.java new file mode 100755 index 0000000..457f104 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/EmptyResponse.java @@ -0,0 +1,22 @@ +package net.jonathangiles.tools.teenyhttpd.implementation; + +import net.jonathangiles.tools.teenyhttpd.model.Header; +import net.jonathangiles.tools.teenyhttpd.model.StatusCode; + +import java.io.BufferedOutputStream; +import java.util.List; + +public class EmptyResponse extends ResponseBase { + public EmptyResponse(final StatusCode statusCode) { + super(statusCode); + } + + public EmptyResponse(final StatusCode statusCode, final List
headers) { + super(statusCode, headers); + } + + @Override + public void writeBody(BufferedOutputStream dataOut) { + // no-op + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/FileResponse.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/FileResponse.java similarity index 60% rename from src/main/java/net/jonathangiles/tools/teenyhttpd/response/FileResponse.java rename to src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/FileResponse.java index 41ceacf..e730256 100755 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/FileResponse.java +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/FileResponse.java @@ -1,47 +1,26 @@ -package net.jonathangiles.tools.teenyhttpd.response; +package net.jonathangiles.tools.teenyhttpd.implementation; import net.jonathangiles.tools.teenyhttpd.TeenyHttpd; -import net.jonathangiles.tools.teenyhttpd.request.Method; -import net.jonathangiles.tools.teenyhttpd.request.Request; +import net.jonathangiles.tools.teenyhttpd.model.ContentType; +import net.jonathangiles.tools.teenyhttpd.model.Headers; +import net.jonathangiles.tools.teenyhttpd.model.Method; +import net.jonathangiles.tools.teenyhttpd.model.Request; +import net.jonathangiles.tools.teenyhttpd.model.StatusCode; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.net.FileNameMap; import java.net.URLConnection; import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -public class FileResponse implements Response { + +public class FileResponse extends ResponseBase { static final String DEFAULT_FILE = "index.html"; static final String FILE_NOT_FOUND = "404.html"; static final String METHOD_NOT_SUPPORTED = "not_supported.html"; - private static final Map contentTypes; - static { - Properties props = new Properties(); - try(InputStream resourceStream = FileResponse.class.getResourceAsStream("contentTypes.properties")) { - props.load(resourceStream); - } catch (IOException e) { - props = null; - e.printStackTrace(); - } - - contentTypes = props == null ? Collections.emptyMap() : (Map) (Object) props; - } - - - private static final FileNameMap FILE_NAME_MAP = URLConnection.getFileNameMap(); - private final StatusCode statusCode; - private final List headers; private File fileToReturn; public FileResponse(final Request request) { @@ -59,9 +38,9 @@ public FileResponse(final Request request) { fileToReturn = getFile(path); if (!fileToReturn.exists()) { fileToReturn = getFile(FILE_NOT_FOUND); - statusCode = StatusCode.NOT_FOUND; + setStatusCode(StatusCode.NOT_FOUND); } else { - statusCode = StatusCode.OK; + setStatusCode(StatusCode.OK); } break; } @@ -74,26 +53,15 @@ public FileResponse(final Request request) { case CONNECT: default: { fileToReturn = getFile(METHOD_NOT_SUPPORTED); - statusCode = StatusCode.NOT_IMPLEMENTED; + setStatusCode(StatusCode.NOT_IMPLEMENTED); break; } } final int fileLength = (int) fileToReturn.length(); - headers = new ArrayList<>(); - headers.add("Content-type: " + getContentType(fileToReturn)); - headers.add("Content-length: " + fileLength); - } - - @Override - public StatusCode getStatusCode() { - return statusCode; - } - - @Override - public List getHeaders() { - return headers; + setHeader(Headers.CONTENT_TYPE.asHeader(getContentType(fileToReturn))); + setHeader(Headers.CONTENT_LENGTH.asHeader(Integer.toString(fileLength))); } @Override @@ -111,14 +79,14 @@ public void writeBody(final BufferedOutputStream dataOut) throws IOException { private String getContentType(final File file) { final String ext = file.getName().substring(file.getName().lastIndexOf(".") + 1); - String contentType = contentTypes.get(ext); + ContentType contentType = ContentType.fromFileExtension(ext); if (contentType != null) { - return contentType; + return contentType.getHeaderValue(); } - contentType = FILE_NAME_MAP.getContentTypeFor(file.getName()); - if (contentType != null) { - return contentType; + String contentTypeString = FILE_NAME_MAP.getContentTypeFor(file.getName()); + if (contentTypeString != null) { + return contentTypeString; } System.err.println("Unable to determine content type for file " + file.getName());; diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/Main.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/Main.java similarity index 96% rename from src/main/java/net/jonathangiles/tools/teenyhttpd/Main.java rename to src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/Main.java index 79d588d..53ae762 100644 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/Main.java +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/Main.java @@ -1,4 +1,6 @@ -package net.jonathangiles.tools.teenyhttpd; +package net.jonathangiles.tools.teenyhttpd.implementation; + +import net.jonathangiles.tools.teenyhttpd.TeenyHttpd; import java.io.File; import java.util.List; diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ResponseBase.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ResponseBase.java new file mode 100644 index 0000000..f1a1d55 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ResponseBase.java @@ -0,0 +1,52 @@ +package net.jonathangiles.tools.teenyhttpd.implementation; + +import net.jonathangiles.tools.teenyhttpd.model.Header; +import net.jonathangiles.tools.teenyhttpd.model.Response; +import net.jonathangiles.tools.teenyhttpd.model.StatusCode; + +import java.util.ArrayList; +import java.util.List; + +public abstract class ResponseBase implements Response { + private StatusCode statusCode; + private List
headers; + + ResponseBase() { + + } + + public ResponseBase(final StatusCode statusCode) { + this(statusCode, new ArrayList<>()); + } + + public ResponseBase(final StatusCode statusCode, final List
headers) { + this.statusCode = statusCode; + this.headers = headers; + } + + @Override + public StatusCode getStatusCode() { + return statusCode; + } + + void setStatusCode(StatusCode statusCode) { + this.statusCode = statusCode; + } + + @Override + public List
getHeaders() { + return headers; + } + + @Override + public void setHeader(Header header) { + if (headers == null) { + headers = new ArrayList<>(); + } + headers.add(header); + } + + void setHeaders(List
headers) { + this.headers = headers; + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ServerSentEventHandlerImpl.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ServerSentEventHandlerImpl.java new file mode 100644 index 0000000..e58ce4b --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ServerSentEventHandlerImpl.java @@ -0,0 +1,70 @@ +package net.jonathangiles.tools.teenyhttpd.implementation; + +import net.jonathangiles.tools.teenyhttpd.model.Request; +import net.jonathangiles.tools.teenyhttpd.model.ServerSentEventHandler; +import net.jonathangiles.tools.teenyhttpd.model.ServerSentEventMessage; + +import java.io.PrintWriter; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Function; + +public abstract class ServerSentEventHandlerImpl implements ServerSentEventHandler { + private boolean isActive = false; + + private final List clients = new CopyOnWriteArrayList<>(); + + /** {@inheritDoc} */ + @Override public final void onConnect(Request request) { + if (!(request instanceof ServerSentEventRequest)) { + throw new IllegalArgumentException("Request must be an instance of ServerSentEventRequest"); + } + clients.add((ServerSentEventRequest) request); + checkState(); + } + + /** {@inheritDoc} */ + @Override public final void onDisconnect(Request request) { + if (!(request instanceof ServerSentEventRequest)) { + throw new IllegalArgumentException("Request must be an instance of ServerSentEventRequest"); + } + clients.remove((ServerSentEventRequest) request); + checkState(); + } + + /** {@inheritDoc} */ + @Override public boolean hasActiveConnections() { + return isActive; + } + + /** {@inheritDoc} */ + @Override public final void sendMessage(final Function messageGenerator) { + if (!hasActiveConnections()) { + return; + } + + clients.forEach(client -> { + PrintWriter writer = client.getWriter(); + if (writer != null) { + if (writer.checkError()) { + // an error here means that the client has disconnected - so we should perform a disconnection + onDisconnect(client); + return; + } + writer.write(messageGenerator.apply(client).toString()); + writer.write("\n\n"); + writer.flush(); + } + }); + } + + private void checkState() { + if (!isActive && !clients.isEmpty()) { + isActive = true; + onActive(); + } else if (isActive && clients.isEmpty()) { + isActive = false; + onInactive(); + } + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ServerSentEventRequest.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ServerSentEventRequest.java new file mode 100644 index 0000000..cd2c9a7 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/ServerSentEventRequest.java @@ -0,0 +1,77 @@ +package net.jonathangiles.tools.teenyhttpd.implementation; + +import net.jonathangiles.tools.teenyhttpd.model.Header; +import net.jonathangiles.tools.teenyhttpd.model.Headers; +import net.jonathangiles.tools.teenyhttpd.model.Method; +import net.jonathangiles.tools.teenyhttpd.model.Request; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.Map; +import java.util.Optional; + +public class ServerSentEventRequest implements Request, Closeable { + private final Request request; + private final PrintWriter out; + + public ServerSentEventRequest(final Request request, final Socket clientSocket) throws IOException { + this.request = request; + this.out = new PrintWriter(clientSocket.getOutputStream()); + } + + public PrintWriter getWriter() { + return out; + } + + @Override + public void close() { + out.close(); + } + + @Override + public Method getMethod() { + return request.getMethod(); + } + + @Override + public String getPath() { + return request.getPath(); + } + +// @Override +// public void addHeader(Header header) { +// request.addHeader(header); +// } + + @Override + public Map getHeaders() { + return request.getHeaders(); + } + + @Override + public Optional
getHeader(String header) { + return request.getHeader(header); + } + + @Override + public Optional
getHeader(Headers header) { + return request.getHeader(header); + } + + @Override + public Map getQueryParams() { + return request.getQueryParams(); + } + + @Override + public Map getPathParams() { + return request.getPathParams(); + } + +// @Override +// public void addPathParam(String name, String value) { +// request.addPathParam(name, value); +// } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/SimpleRequest.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/SimpleRequest.java new file mode 100644 index 0000000..d84d4d8 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/SimpleRequest.java @@ -0,0 +1,120 @@ +package net.jonathangiles.tools.teenyhttpd.implementation; + +import net.jonathangiles.tools.teenyhttpd.model.Header; +import net.jonathangiles.tools.teenyhttpd.model.Headers; +import net.jonathangiles.tools.teenyhttpd.model.Method; +import net.jonathangiles.tools.teenyhttpd.model.QueryParams; +import net.jonathangiles.tools.teenyhttpd.model.Request; + +import java.util.*; + +/** + * Represents an incoming request. + */ +public class SimpleRequest implements Request { + private final Method method; + private final String path; + private final QueryParams queryParams; + + // These are 'raw' headers - they are not yet parsed into a map of headers + private List
headers; + private Map headersMap; + + private Map pathParams; // FIXME: this is a hack + + public SimpleRequest(final Method method, final String path, final QueryParams queryParams) { + this.method = method; + this.path = path; + this.queryParams = queryParams; + } + + public SimpleRequest(final Method method, + final String path, + final QueryParams queryParams, + final Map headers) { + this.method = method; + this.path = path; + this.queryParams = queryParams; + this.headersMap = headers; + } + + public SimpleRequest(Method method, String path, QueryParams queryParams, final Map headers, Map pathParams) { + this.method = method; + this.path = path; + this.queryParams = queryParams; + this.headersMap = headers; + this.pathParams = Collections.unmodifiableMap(pathParams); + } + + public SimpleRequest(Method method, String path, QueryParams queryParams, final List
headers, + Map pathParams) { + this.method = method; + this.path = path; + this.queryParams = queryParams; + this.headers = headers; + this.pathParams = Collections.unmodifiableMap(pathParams); + } + + @Override public Method getMethod() { + return method; + } + + @Override public String getPath() { + return path; + } + +// public void addHeader(final Header header) { +// if (headersMap == null) { +// headersMap = new LinkedHashMap<>(); +// } +// headersMap.put(header.getKey(), header); +// } + + /** + * Returns a read-only Map of headers. + * @return + */ + @Override public Map getHeaders() { + if (headersMap == null) { + if (headers != null) { + headersMap = new LinkedHashMap<>(); + headers.forEach(header -> headersMap.put(header.getKey(), header)); + headersMap = Collections.unmodifiableMap(headersMap); + headers = null; + } else { + return Collections.emptyMap(); + } + } + return Collections.unmodifiableMap(headersMap); + } + + @Override public Optional
getHeader(final String header) { + return Optional.ofNullable(getHeaders().get(header)); + } + + @Override public Optional
getHeader(final Headers header) { + return Optional.ofNullable(getHeaders().get(header.getKey())); + } + + @Override public Map getQueryParams() { + return queryParams.getQueryParams(); + } + + @Override + public String toString() { + return "Request{" + + "method=" + method + + ", path='" + path + '\'' + + ", queryParams=" + queryParams + + ", headers=" + headersMap + + '}'; + } + + @Override public Map getPathParams() { + return pathParams == null ? Collections.emptyMap() : pathParams; + } + +// @Override public void addPathParam(String name, String value) { +// pathParams.put(name, value); +// } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/StringResponse.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/StringResponse.java similarity index 55% rename from src/main/java/net/jonathangiles/tools/teenyhttpd/response/StringResponse.java rename to src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/StringResponse.java index 6249ca2..46ed06b 100755 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/StringResponse.java +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/implementation/StringResponse.java @@ -1,19 +1,20 @@ -package net.jonathangiles.tools.teenyhttpd.response; +package net.jonathangiles.tools.teenyhttpd.implementation; -import net.jonathangiles.tools.teenyhttpd.request.Request; +import net.jonathangiles.tools.teenyhttpd.model.Header; +import net.jonathangiles.tools.teenyhttpd.model.StatusCode; import java.util.List; public class StringResponse extends ByteResponse { private static final byte[] EMPTY_BYTE_ARRAY = new byte[] { }; - public StringResponse(final StatusCode statusCode) { - super(statusCode); - } +// public StringResponse(final StatusCode statusCode) { +// super(statusCode); +// } - public StringResponse(final StatusCode statusCode, final List headers) { - super(statusCode, headers); - } +// public StringResponse(final StatusCode statusCode, final List
headers) { +// super(statusCode, headers); +// } public StringResponse(final String body) { this(StatusCode.OK, body); @@ -23,7 +24,7 @@ public StringResponse(final StatusCode statusCode, final String body) { super(statusCode, body == null ? EMPTY_BYTE_ARRAY : body.getBytes()); } - public StringResponse(final StatusCode statusCode, final List headers, final String body) { + public StringResponse(final StatusCode statusCode, final List
headers, final String body) { super(statusCode, headers, body == null ? EMPTY_BYTE_ARRAY : body.getBytes()); } } diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/model/ContentType.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/ContentType.java new file mode 100644 index 0000000..bf6fd75 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/ContentType.java @@ -0,0 +1,835 @@ +package net.jonathangiles.tools.teenyhttpd.model; + +import java.util.*; + +public enum ContentType { + // SSE events: + EVENT_STREAM("text/event-stream"), + + // Common file types: + A("application/octet-stream", "a"), + AAC("audio/x-aac", "aac"), + AI("application/postscript", "ai"), + AIF("audio/x-aiff", "aif", "aifc", "aiff"), + APK("application/vnd.android.package-archive", "apk"), + APPLICATION("application/x-ms-application", "application"), + ASC("application/pgp-signature", "asc"), + ASF("video/x-ms-asf", "asf"), + ASM("text/x-asm", "asm"), + ASO("application/vnd.accpac.simply.aso", "aso"), + ASX("video/x-ms-asf", "asx"), + ATC("application/vnd.acucorp", "atc"), + ATOM("application/atom+xml", "atom"), + ATOMCAT("application/atomcat+xml", "atomcat"), + ATOMSVC("application/atomsvc+xml", "atomsvc"), + ATX("application/vnd.antix.game-component", "atx"), + AU("audio/basic", "au"), + AVI("video/x-msvideo", "avi"), + AW("application/applixware", "aw"), + AZF("application/vnd.airzip.filesecure.azf", "azf"), + AZS("application/vnd.airzip.filesecure.azs", "azs"), + AZW("application/vnd.amazon.ebook", "azw"), + BAT("application/x-msdownload", "bat"), + BCPIO("application/x-bcpio", "bcpio"), + BDF("application/x-font-bdf", "bdf"), + BDM("application/vnd.syncml.dm+wbxml", "bdm"), + BH2("application/vnd.fujitsu.oasysprs", "bh2"), + BIN("application/octet-stream", "bin"), + BMI("application/vnd.bmi", "bmi"), + BMP("image/bmp", "bmp"), + BOOK("application/vnd.framemaker", "book"), + BOX("application/vnd.previewsystems.box", "box"), + BOZ("application/x-bzip2", "boz"), + BPK("application/octet-stream", "bpk"), + BTIF("image/prs.btif", "btif"), + BZ("application/x-bzip", "bz"), + BZ2("application/x-bzip2", "bz2"), + C("text/x-c", "c"), + C4D("application/vnd.clonk.c4group", "c4d"), + C4F("application/vnd.clonk.c4group", "c4f"), + C4G("application/vnd.clonk.c4group", "c4g"), + C4P("application/vnd.clonk.c4group", "c4p"), + C4U("application/vnd.clonk.c4group", "c4u"), + CAB("application/vnd.ms-cab-compressed", "cab"), + CAR("application/vnd.curl.car", "car"), + CAT("application/vnd.ms-pki.seccat", "cat"), + CC("text/x-c", "cc"), + CCT("application/x-director", "cct"), + CCXML("application/ccxml+xml", "ccxml"), + CDBCMSG("application/vnd.contact.cmsg", "cdbcmsg"), + CDF("application/x-netcdf", "cdf"), + CDKEY("application/vnd.mediastation.cdkey", "cdkey"), + CDX("chemical/x-cdx", "cdx"), + CDXML("application/vnd.chemdraw+xml", "cdxml"), + CDY("application/vnd.cinderella", "cdy"), + CER("application/pkix-cert", "cer"), + CGM("image/cgm", "cgm"), + CHAT("application/x-chat", "chat"), + CHM("application/vnd.ms-htmlhelp", "chm"), + CHRT("application/vnd.kde.kchart", "chrt"), + CIF("chemical/x-cif", "cif"), + CII("application/vnd.anser-web-certificate-issue-initiation", "cii"), + CIL("application/vnd.ms-artgalry", "cil"), + CLA("application/vnd.claymore", "cla"), + CLASS("application/java-vm", "class"), + CLKK("application/vnd.crick.clicker.keyboard", "clkk"), + CLKP("application/vnd.crick.clicker.palette", "clkp"), + CLKT("application/vnd.crick.clicker.template", "clkt"), + CLKW("application/vnd.crick.clicker.wordbank", "clkw"), + CLKX("application/vnd.crick.clicker", "clkx"), + CLP("application/x-msclip", "clp"), + CMC("application/vnd.cosmocaller", "cmc"), + CMDF("chemical/x-cmdf", "cmdf"), + CML("chemical/x-cml", "cml"), + CMP("application/vnd.yellowriver-custom-menu", "cmp"), + CMX("image/x-cmx", "cmx"), + COD("application/vnd.rim.cod", "cod"), + COM("application/x-msdownload", "com"), + CONF("text/plain", "conf"), + CPIO("application/x-cpio", "cpio"), + CPP("text/x-c", "cpp"), + CPT("application/mac-compactpro", "cpt"), + CRD("application/x-mscardfile", "crd"), + CRL("application/pkix-crl", "crl"), + CRT("application/x-x509-ca-cert", "crt"), + CSH("application/x-csh", "csh"), + CSML("chemical/x-csml", "csml"), + CSP("application/vnd.commonspace", "csp"), + CSS("text/css", "css"), + CST("application/x-director", "cst"), + CSV("text/csv", "csv"), + CU("application/cu-seeme", "cu"), + CURL("text/vnd.curl", "curl"), + CWW("application/prs.cww", "cww"), + CXT("application/x-director", "cxt"), + CXX("text/x-c", "cxx"), + DAF("application/vnd.mobius.daf", "daf"), + DATALESS("application/vnd.fdsn.seed", "dataless"), + DAVMOUNT("application/davmount+xml", "davmount"), + DCR("application/x-director", "dcr"), + DCURL("text/vnd.curl.dcurl", "dcurl"), + DD2("application/vnd.oma.dd2+xml", "dd2"), + DDD("application/vnd.fujixerox.ddd", "ddd"), + DEB("application/x-debian-package", "deb"), + DEF("text/plain", "def"), + DEPLOY("application/octet-stream", "deploy"), + DER("application/x-x509-ca-cert", "der"), + DFAC("application/vnd.dreamfactory", "dfac"), + DIC("text/x-c", "dic"), + DIFF("text/plain", "diff"), + DIR("application/x-director", "dir"), + DIS("application/vnd.mobius.dis", "dis"), + DIST("application/octet-stream", "dist"), + DISTZ("application/octet-stream", "distz"), + DJV("image/vnd.djvu", "djv"), + DJVU("image/vnd.djvu", "djvu"), + DLL("application/x-msdownload", "dll"), + DMG("application/octet-stream", "dmg"), + DMS("application/octet-stream", "dms"), + DNA("application/vnd.dna", "dna"), + DOC("application/msword", "doc"), + DOCM("application/vnd.ms-word.document.macroenabled.12", "docm"), + DOCX("application/vnd.openxmlformats-officedocument.wordprocessingml.document", "docx"), + DOT("application/msword", "dot"), + DOTM("application/vnd.ms-word.template.macroenabled.12", "dotm"), + DOTX("application/vnd.openxmlformats-officedocument.wordprocessingml.template", "dotx"), + DP("application/vnd.osgi.dp", "dp"), + DPG("application/vnd.dpgraph", "dpg"), + DSC("text/prs.lines.tag", "dsc"), + DTB("application/x-dtbook+xml", "dtb"), + DTD("application/xml-dtd", "dtd"), + DTS("audio/vnd.dts", "dts"), + DTSHD("audio/vnd.dts.hd", "dtshd"), + DUMP("application/octet-stream", "dump"), + DVI("application/x-dvi", "dvi"), + DWF("model/vnd.dwf", "dwf"), + DWG("image/vnd.dwg", "dwg"), + DXF("image/vnd.dxf", "dxf"), + DXP("application/vnd.spotfire.dxp", "dxp"), + DXR("application/x-director", "dxr"), + ECELP4800("audio/vnd.nuera.ecelp4800", "ecelp4800"), + ECELP7470("audio/vnd.nuera.ecelp7470", "ecelp7470"), + ECELP9600("audio/vnd.nuera.ecelp9600", "ecelp9600"), + ECMA("application/ecmascript", "ecma"), + EDM("application/vnd.novadigm.edm", "edm"), + EDX("application/vnd.novadigm.edx", "edx"), + EFIF("application/vnd.picsel", "efif"), + EI6("application/vnd.pg.osasli", "ei6"), + ELC("application/octet-stream", "elc"), + EML("message/rfc822", "eml"), + EMMA("application/emma+xml", "emma"), + EOL("audio/vnd.digital-winds", "eol"), + EOT("application/vnd.ms-fontobject", "eot"), + EPS("application/postscript", "eps"), + EPUB("application/epub+zip", "epub"), + ES3("application/vnd.eszigno3+xml", "es3"), + ESF("application/vnd.epson.esf", "esf"), + ET3("application/vnd.eszigno3+xml", "et3"), + ETX("text/x-setext", "etx"), + EXE("application/x-msdownload", "exe"), + EXT("application/vnd.novadigm.ext", "ext"), + EZ("application/andrew-inset", "ez"), + EZ2("application/vnd.ezpix-album", "ez2"), + EZ3("application/vnd.ezpix-package", "ez3"), + F("text/x-fortran", "f"), + F4V("video/x-f4v", "f4v"), + F77("text/x-fortran", "f77"), + F90("text/x-fortran", "f90"), + FBS("image/vnd.fastbidsheet", "fbs"), + FDF("application/vnd.fdf", "fdf"), + FE_LAUNCH("application/vnd.denovo.fcselayout-link", "fe_launch"), + FG5("application/vnd.fujitsu.oasysgp", "fg5"), + FGD("application/x-director", "fgd"), + FH("image/x-freehand", "fh"), + FH4("image/x-freehand", "fh4"), + FH5("image/x-freehand", "fh5"), + FH7("image/x-freehand", "fh7"), + FHC("image/x-freehand", "fhc"), + FIG("application/x-xfig", "fig"), + FLI("video/x-fli", "fli"), + FLO("application/vnd.micrografx.flo", "flo"), + FLV("video/x-flv", "flv"), + FLW("application/vnd.kde.kivio", "flw"), + FLX("text/vnd.fmi.flexstor", "flx"), + FLY("text/vnd.fly", "fly"), + FM("application/vnd.framemaker", "fm"), + FNC("application/vnd.frogans.fnc", "fnc"), + FOR("text/x-fortran", "for"), + FPX("image/vnd.fpx", "fpx"), + FRAME("application/vnd.framemaker", "frame"), + FSC("application/vnd.fsc.weblaunch", "fsc"), + FST("image/vnd.fst", "fst"), + FTC("application/vnd.fluxtime.clip", "ftc"), + FTI("application/vnd.anser-web-funds-transfer-initiation", "fti"), + FVT("video/vnd.fvt", "fvt"), + FZS("application/vnd.fuzzysheet", "fzs"), + G3("image/g3fax", "g3"), + GAC("application/vnd.groove-account", "gac"), + GDL("model/vnd.gdl", "gdl"), + GEO("application/vnd.dynageo", "geo"), + GEX("application/vnd.geometry-explorer", "gex"), + GGB("application/vnd.geogebra.file", "ggb"), + GGT("application/vnd.geogebra.tool", "ggt"), + GHF("application/vnd.groove-help", "ghf"), + GIF("image/gif", "gif"), + GIM("application/vnd.groove-identity-message", "gim"), + GMX("application/vnd.gmx", "gmx"), + GNUMERIC("application/x-gnumeric", "gnumeric"), + GPH("application/vnd.flographit", "gph"), + GQF("application/vnd.grafeq", "gqf"), + GQS("application/vnd.grafeq", "gqs"), + GRAM("application/srgs", "gram"), + GRE("application/vnd.geometry-explorer", "gre"), + GRV("application/vnd.groove-injector", "grv"), + GRXML("application/srgs+xml", "grxml"), + GSF("application/x-font-ghostscript", "gsf"), + GTAR("application/x-gtar", "gtar"), + GTM("application/vnd.groove-tool-message", "gtm"), + GTW("model/vnd.gtw", "gtw"), + GV("text/vnd.graphviz", "gv"), + GZ("application/x-gzip", "gz"), + H("text/x-c", "h"), + H261("video/h261", "h261"), + H263("video/h263", "h263"), + H264("video/h264", "h264"), + HBCI("application/vnd.hbci", "hbci"), + HDF("application/x-hdf", "hdf"), + HH("text/x-c", "hh"), + HLP("application/winhlp", "hlp"), + HPGL("application/vnd.hp-hpgl", "hpgl"), + HPID("application/vnd.hp-hpid", "hpid"), + HPS("application/vnd.hp-hps", "hps"), + HQX("application/mac-binhex40", "hqx"), + HTKE("application/vnd.kenameaapp", "htke"), + HTML("text/html", "html", "htm"), + HVD("application/vnd.yamaha.hv-dic", "hvd"), + HVP("application/vnd.yamaha.hv-voice", "hvp"), + HVS("application/vnd.yamaha.hv-script", "hvs"), + ICC("application/vnd.iccprofile", "icc"), + ICE("x-conference/x-cooltalk", "ice"), + ICM("application/vnd.iccprofile", "icm"), + ICO("image/x-icon", "ico"), + ICS("text/calendar", "ics"), + IEF("image/ief", "ief"), + IFB("text/calendar", "ifb"), + IFM("application/vnd.shana.informed.formdata", "ifm"), + IGES("model/iges", "iges"), + IGL("application/vnd.igloader", "igl"), + IGS("model/iges", "igs"), + IGX("application/vnd.micrografx.igx", "igx"), + IIF("application/vnd.shana.informed.interchange", "iif"), + IMP("application/vnd.accpac.simply.imp", "imp"), + IMS("application/vnd.ms-ims", "ims"), + IN("text/plain", "in"), + IPK("application/vnd.shana.informed.package", "ipk"), + IRM("application/vnd.ibm.rights-management", "irm"), + IRP("application/vnd.irepository.package+xml", "irp"), + ISO("application/octet-stream", "iso"), + ITP("application/vnd.shana.informed.formtemplate", "itp"), + IVP("application/vnd.immervision-ivp", "ivp"), + IVU("application/vnd.immervision-ivu", "ivu"), + JAD("text/vnd.sun.j2me.app-descriptor", "jad"), + JAM("application/vnd.jam", "jam"), + JAR("application/java-archive", "jar"), + JAVA("text/x-java-source", "java"), + JISP("application/vnd.jisp", "jisp"), + JLT("application/vnd.hp-jlyt", "jlt"), + JNLP("application/x-java-jnlp-file", "jnlp"), + JODA("application/vnd.joost.joda-archive", "joda"), + JPEG("image/jpeg", "jpe", "jpg", "jpeg"), + JPGM("video/jpm", "jpgm"), + JPGV("video/jpeg", "jpgv"), + JPM("video/jpm", "jpm"), + JS("application/javascript", "js"), + JSON("application/json", "json"), + KAR("audio/midi", "kar"), + KARBON("application/vnd.kde.karbon", "karbon"), + KFO("application/vnd.kde.kformula", "kfo"), + KIA("application/vnd.kidspiration", "kia"), + KIL("application/x-killustrator", "kil"), + KML("application/vnd.google-earth.kml+xml", "kml"), + KMZ("application/vnd.google-earth.kmz", "kmz"), + KNE("application/vnd.kinar", "kne"), + KNP("application/vnd.kinar", "knp"), + KON("application/vnd.kde.kontour", "kon"), + KPR("application/vnd.kde.kpresenter", "kpr"), + KPT("application/vnd.kde.kpresenter", "kpt"), + KSH("text/plain", "ksh"), + KSP("application/vnd.kde.kspread", "ksp"), + KTR("application/vnd.kahootz", "ktr"), + KTZ("application/vnd.kahootz", "ktz"), + KWD("application/vnd.kde.kword", "kwd"), + KWT("application/vnd.kde.kword", "kwt"), + LATEX("application/x-latex", "latex"), + LBD("application/vnd.llamagraphics.life-balance.desktop", "lbd"), + LBE("application/vnd.llamagraphics.life-balance.exchange+xml", "lbe"), + LES("application/vnd.hhe.lesson-player", "les"), + LHA("application/octet-stream", "lha"), + LINK66("application/vnd.route66.link66+xml", "link66"), + LIST("text/plain", "list"), + LIST3820("application/vnd.ibm.modcap", "list3820"), + LISTAFP("application/vnd.ibm.modcap", "listafp"), + LOG("text/plain", "log"), + LOSTXML("application/lost+xml", "lostxml"), + LRF("application/octet-stream", "lrf"), + LRM("application/vnd.ms-lrm", "lrm"), + LTF("application/vnd.frogans.ltf", "ltf"), + LVP("audio/vnd.lucent.voice", "lvp"), + LWP("application/vnd.lotus-wordpro", "lwp"), + LZH("application/octet-stream", "lzh"), + M13("application/x-msmediaview", "m13"), + M14("application/x-msmediaview", "m14"), + M1V("video/mpeg", "m1v"), + M2A("audio/mpeg", "m2a"), + M2V("video/mpeg", "m2v"), + M3A("audio/mpeg", "m3a"), + M3U("audio/x-mpegurl", "m3u"), + M4U("video/vnd.mpegurl", "m4u"), + M4V("video/x-m4v", "m4v"), + MA("application/mathematica", "ma"), + MAG("application/vnd.ecowin.chart", "mag"), + MAKER("application/vnd.framemaker", "maker"), + MAN("text/troff", "man"), + MATHML("application/mathml+xml", "mathml"), + MB("application/mathematica", "mb"), + MBK("application/vnd.mobius.mbk", "mbk"), + MBOX("application/mbox", "mbox"), + MC1("application/vnd.medcalcdata", "mc1"), + MCD("application/vnd.mcd", "mcd"), + MCURL("text/vnd.curl.mcurl", "mcurl"), + MDB("application/x-msaccess", "mdb"), + MDI("image/vnd.ms-modi", "mdi"), + ME("text/troff", "me"), + MESH("model/mesh", "mesh"), + MFM("application/vnd.mfmp", "mfm"), + MGZ("application/vnd.proteus.magazine", "mgz"), + MHT("message/rfc822", "mht"), + MHTML("message/rfc822", "mhtml"), + MID("audio/midi", "mid"), + MIDI("audio/midi", "midi"), + MIF("application/vnd.mif", "mif"), + MIME("message/rfc822", "mime"), + MJ2("video/mj2", "mj2"), + MJP2("video/mj2", "mjp2"), + MLP("application/vnd.dolby.mlp", "mlp"), + MMD("application/vnd.chipnuts.karaoke-mmd", "mmd"), + MMF("application/vnd.smaf", "mmf"), + MMR("image/vnd.fujixerox.edmics-mmr", "mmr"), + MNY("application/x-msmoney", "mny"), + MOBI("application/x-mobipocket-ebook", "mobi"), + MOV("video/quicktime", "mov"), + MOVIE("video/x-sgi-movie", "movie"), + MP2("audio/mpeg", "mp2"), + MP2A("audio/mpeg", "mp2a"), + MP3("audio/mpeg", "mp3"), + MP4("video/mp4", "mp4"), + MP4A("audio/mp4", "mp4a"), + MP4S("application/mp4", "mp4s"), + MP4V("video/mp4", "mp4v"), + MPA("video/mpeg", "mpa"), + MPC("application/vnd.mophun.certificate", "mpc"), + MPE("video/mpeg", "mpe"), + MPEG("video/mpeg", "mpeg"), + MPG("video/mpeg", "mpg"), + MPG4("video/mp4", "mpg4"), + MPGA("audio/mpeg", "mpga"), + MPKG("application/vnd.apple.installer+xml", "mpkg"), + MPM("application/vnd.blueice.multipass", "mpm"), + MPN("application/vnd.mophun.application", "mpn"), + MPP("application/vnd.ms-project", "mpp"), + MPT("application/vnd.ms-project", "mpt"), + MPY("application/vnd.ibm.minipay", "mpy"), + MQY("application/vnd.mobius.mqy", "mqy"), + MRC("application/marc", "mrc"), + MS("text/troff", "ms"), + MSCML("application/mediaservercontrol+xml", "mscml"), + MSEED("application/vnd.fdsn.mseed", "mseed"), + MSEQ("application/vnd.mseq", "mseq"), + MSF("application/vnd.epson.msf", "msf"), + MSH("model/mesh", "msh"), + MSI("application/x-msdownload", "msi"), + MSL("application/vnd.mobius.msl", "msl"), + MSTY("application/vnd.muvee.style", "msty"), + MTS("model/vnd.mts", "mts"), + MUS("application/vnd.musician", "mus"), + MUSICXML("application/vnd.recordare.musicxml+xml", "musicxml"), + MVB("application/x-msmediaview", "mvb"), + MWF("application/vnd.mfer", "mwf"), + MXF("application/mxf", "mxf"), + MXL("application/vnd.recordare.musicxml", "mxl"), + MXML("application/xv+xml", "mxml"), + MXS("application/vnd.triscape.mxs", "mxs"), + MXU("video/vnd.mpegurl", "mxu"), + N_GAGE("application/vnd.nokia.n-gage.symbian.install", "n-gage"), + NB("application/mathematica", "nb"), + NC("application/x-netcdf", "nc"), + NCX("application/x-dtbncx+xml", "ncx"), + NGDAT("application/vnd.nokia.n-gage.data", "ngdat"), + NLU("application/vnd.neurolanguage.nlu", "nlu"), + NML("application/vnd.enliven", "nml"), + NND("application/vnd.noblenet-directory", "nnd"), + NNS("application/vnd.noblenet-sealer", "nns"), + NNW("application/vnd.noblenet-web", "nnw"), + NPX("image/vnd.net-fpx", "npx"), + NSF("application/vnd.lotus-notes", "nsf"), + NWS("message/rfc822", "nws"), + O("application/octet-stream", "o"), + OA2("application/vnd.fujitsu.oasys2", "oa2"), + OA3("application/vnd.fujitsu.oasys3", "oa3"), + OAS("application/vnd.fujitsu.oasys", "oas"), + OBD("application/x-msbinder", "obd"), + OBJ("application/octet-stream", "obj"), + ODA("application/oda", "oda"), + ODB("application/vnd.oasis.opendocument.database", "odb"), + ODC("application/vnd.oasis.opendocument.chart", "odc"), + ODF("application/vnd.oasis.opendocument.formula", "odf"), + ODFT("application/vnd.oasis.opendocument.formula-template", "odft"), + ODG("application/vnd.oasis.opendocument.graphics", "odg"), + ODI("application/vnd.oasis.opendocument.image", "odi"), + ODP("application/vnd.oasis.opendocument.presentation", "odp"), + ODS("application/vnd.oasis.opendocument.spreadsheet", "ods"), + ODT("application/vnd.oasis.opendocument.text", "odt"), + OGA("audio/ogg", "oga"), + OGG("audio/ogg", "ogg"), + OGV("video/ogg", "ogv"), + OGX("application/ogg", "ogx"), + ONEPKG("application/onenote", "onepkg"), + ONETMP("application/onenote", "onetmp"), + ONETOC("application/onenote", "onetoc"), + ONETOC2("application/onenote", "onetoc2"), + OPF("application/oebps-package+xml", "opf"), + OPRC("application/vnd.palm", "oprc"), + ORG("application/vnd.lotus-organizer", "org"), + OSF("application/vnd.yamaha.openscoreformat", "osf"), + OSFPVG("application/vnd.yamaha.openscoreformat.osfpvg+xml", "osfpvg"), + OTC("application/vnd.oasis.opendocument.chart-template", "otc"), + OTF("application/x-font-otf", "otf"), + OTG("application/vnd.oasis.opendocument.graphics-template", "otg"), + OTH("application/vnd.oasis.opendocument.text-web", "oth"), + OTI("application/vnd.oasis.opendocument.image-template", "oti"), + OTM("application/vnd.oasis.opendocument.text-master", "otm"), + OTP("application/vnd.oasis.opendocument.presentation-template", "otp"), + OTS("application/vnd.oasis.opendocument.spreadsheet-template", "ots"), + OTT("application/vnd.oasis.opendocument.text-template", "ott"), + OXT("application/vnd.openofficeorg.extension", "oxt"), + P("text/x-pascal", "p"), + P10("application/pkcs10", "p10"), + P12("application/x-pkcs12", "p12"), + P7B("application/x-pkcs7-certificates", "p7b"), + P7C("application/pkcs7-mime", "p7c"), + P7M("application/pkcs7-mime", "p7m"), + P7R("application/x-pkcs7-certreqresp", "p7r"), + P7S("application/pkcs7-signature", "p7s"), + PAS("text/x-pascal", "pas"), + PBD("application/vnd.powerbuilder6", "pbd"), + PBM("image/x-portable-bitmap", "pbm"), + PCF("application/x-font-pcf", "pcf"), + PCL("application/vnd.hp-pcl", "pcl"), + PCLXL("application/vnd.hp-pclxl", "pclxl"), + PCT("image/x-pict", "pct"), + PCURL("application/vnd.curl.pcurl", "pcurl"), + PCX("image/x-pcx", "pcx"), + PDB("application/vnd.palm", "pdb"), + PDF("application/pdf", "pdf"), + PFA("application/x-font-type1", "pfa"), + PFB("application/x-font-type1", "pfb"), + PFM("application/x-font-type1", "pfm"), + PFR("application/font-tdpfr", "pfr"), + PFX("application/x-pkcs12", "pfx"), + PGM("image/x-portable-graymap", "pgm"), + PGN("application/x-chess-pgn", "pgn"), + PGP("application/pgp-encrypted", "pgp"), + PIC("image/x-pict", "pic"), + PKG("application/octet-stream", "pkg"), + PKI("application/pkixcmp", "pki"), + PKIPATH("application/pkix-pkipath", "pkipath"), + PL("text/plain", "pl"), + PLB("application/vnd.3gpp.pic-bw-large", "plb"), + PLC("application/vnd.mobius.plc", "plc"), + PLF("application/vnd.pocketlearn", "plf"), + PLS("application/pls+xml", "pls"), + PML("application/vnd.ctc-posml", "pml"), + PNG("image/png", "png"), + PNM("image/x-portable-anymap", "pnm"), + PORTPKG("application/vnd.macports.portpkg", "portpkg"), + POT("application/vnd.ms-powerpoint", "pot"), + POTM("application/vnd.ms-powerpoint.template.macroenabled.12", "potm"), + POTX("application/vnd.openxmlformats-officedocument.presentationml.template", "potx"), + PPA("application/vnd.ms-powerpoint", "ppa"), + PPAM("application/vnd.ms-powerpoint.addin.macroenabled.12", "ppam"), + PPD("application/vnd.cups-ppd", "ppd"), + PPM("image/x-portable-pixmap", "ppm"), + PPS("application/vnd.ms-powerpoint", "pps"), + PPSM("application/vnd.ms-powerpoint.slideshow.macroenabled.12", "ppsm"), + PPSX("application/vnd.openxmlformats-officedocument.presentationml.slideshow", "ppsx"), + PPT("application/vnd.ms-powerpoint", "ppt"), + PPTM("application/vnd.ms-powerpoint.presentation.macroenabled.12", "pptm"), + PPTX("application/vnd.openxmlformats-officedocument.presentationml.presentation", "pptx"), + PQA("application/vnd.palm", "pqa"), + PRC("application/x-mobipocket-ebook", "prc"), + PRE("application/vnd.lotus-freelance", "pre"), + PRF("application/pics-rules", "prf"), + PS("application/postscript", "ps"), + PSB("application/vnd.3gpp.pic-bw-small", "psb"), + PSD("image/vnd.adobe.photoshop", "psd"), + PSF("application/x-font-linux-psf", "psf"), + PTID("application/vnd.pvi.ptid1", "ptid"), + PUB("application/x-mspublisher", "pub"), + PVB("application/vnd.3gpp.pic-bw-var", "pvb"), + PWN("application/vnd.3m.post-it-notes", "pwn"), + PWZ("application/vnd.ms-powerpoint", "pwz"), + PY("text/x-python", "py"), + PYA("audio/vnd.ms-playready.media.pya", "pya"), + PYC("application/x-python-code", "pyc"), + PYO("application/x-python-code", "pyo"), + PYV("video/vnd.ms-playready.media.pyv", "pyv"), + QAM("application/vnd.epson.quickanime", "qam"), + QBO("application/vnd.intu.qbo", "qbo"), + QFX("application/vnd.intu.qfx", "qfx"), + QPS("application/vnd.publishare-delta-tree", "qps"), + QT("video/quicktime", "qt"), + QWD("application/vnd.quark.quarkxpress", "qwd"), + QWT("application/vnd.quark.quarkxpress", "qwt"), + QXB("application/vnd.quark.quarkxpress", "qxb"), + QXD("application/vnd.quark.quarkxpress", "qxd"), + QXL("application/vnd.quark.quarkxpress", "qxl"), + QXT("application/vnd.quark.quarkxpress", "qxt"), + RA("audio/x-pn-realaudio", "ra"), + RAM("audio/x-pn-realaudio", "ram"), + RAR("application/x-rar-compressed", "rar"), + RAS("image/x-cmu-raster", "ras"), + RCPROFILE("application/vnd.ipunplugged.rcprofile", "rcprofile"), + RDF("application/rdf+xml", "rdf"), + RDZ("application/vnd.data-vision.rdz", "rdz"), + REP("application/vnd.businessobjects", "rep"), + RES("application/x-dtbresource+xml", "res"), + RGB("image/x-rgb", "rgb"), + RIF("application/reginfo+xml", "rif"), + RL("application/resource-lists+xml", "rl"), + RLC("image/vnd.fujixerox.edmics-rlc", "rlc"), + RLD("application/resource-lists-diff+xml", "rld"), + RM("application/vnd.rn-realmedia", "rm"), + RMI("audio/midi", "rmi"), + RMP("audio/x-pn-realaudio-plugin", "rmp"), + RMS("application/vnd.jcp.javame.midlet-rms", "rms"), + RNC("application/relax-ng-compact-syntax", "rnc"), + ROFF("text/troff", "roff"), + RPM("application/x-rpm", "rpm"), + RPSS("application/vnd.nokia.radio-presets", "rpss"), + RPST("application/vnd.nokia.radio-preset", "rpst"), + RQ("application/sparql-query", "rq"), + RS("application/rls-services+xml", "rs"), + RSD("application/rsd+xml", "rsd"), + RSS("application/rss+xml", "rss"), + RTF("application/rtf", "rtf"), + RTX("text/richtext", "rtx"), + S("text/x-asm", "s"), + SAF("application/vnd.yamaha.smaf-audio", "saf"), + SBML("application/sbml+xml", "sbml"), + SC("application/vnd.ibm.secure-container", "sc"), + SCD("application/x-msschedule", "scd"), + SCM("application/vnd.lotus-screencam", "scm"), + SCQ("application/scvp-cv-request", "scq"), + SCS("application/scvp-cv-response", "scs"), + SCURL("text/vnd.curl.scurl", "scurl"), + SDA("application/vnd.stardivision.draw", "sda"), + SDC("application/vnd.stardivision.calc", "sdc"), + SDD("application/vnd.stardivision.impress", "sdd"), + SDKD("application/vnd.solent.sdkm+xml", "sdkd"), + SDKM("application/vnd.solent.sdkm+xml", "sdkm"), + SDP("application/sdp", "sdp"), + SDW("application/vnd.stardivision.writer", "sdw"), + SEE("application/vnd.seemail", "see"), + SEED("application/vnd.fdsn.seed", "seed"), + SEMA("application/vnd.sema", "sema"), + SEMD("application/vnd.semd", "semd"), + SEMF("application/vnd.semf", "semf"), + SER("application/java-serialized-object", "ser"), + SETPAY("application/set-payment-initiation", "setpay"), + SETREG("application/set-registration-initiation", "setreg"), + SFD_HDSTX("application/vnd.hydrostatix.sof-data", "sfd-hdstx"), + SFS("application/vnd.spotfire.sfs", "sfs"), + SGL("application/vnd.stardivision.writer-global", "sgl"), + SGM("text/sgml", "sgm"), + SGML("text/sgml", "sgml"), + SH("application/x-sh", "sh"), + SHAR("application/x-shar", "shar"), + SHF("application/shf+xml", "shf"), + SI("text/vnd.wap.si", "si"), + SIC("application/vnd.wap.sic", "sic"), + SIG("application/pgp-signature", "sig"), + SILO("model/mesh", "silo"), + SIS("application/vnd.symbian.install", "sis"), + SISX("application/vnd.symbian.install", "sisx"), + SIT("application/x-stuffit", "sit"), + SITX("application/x-stuffitx", "sitx"), + SKD("application/vnd.koan", "skd"), + SKM("application/vnd.koan", "skm"), + SKP("application/vnd.koan", "skp"), + SKT("application/vnd.koan", "skt"), + SL("text/vnd.wap.sl", "sl"), + SLC("application/vnd.wap.slc", "slc"), + SLDM("application/vnd.ms-powerpoint.slide.macroenabled.12", "sldm"), + SLDX("application/vnd.openxmlformats-officedocument.presentationml.slide", "sldx"), + SLT("application/vnd.epson.salt", "slt"), + SMF("application/vnd.stardivision.math", "smf"), + SMI("application/smil+xml", "smi"), + SMIL("application/smil+xml", "smil"), + SND("audio/basic", "snd"), + SNF("application/x-font-snf", "snf"), + SO("application/octet-stream", "so"), + SPC("application/x-pkcs7-certificates", "spc"), + SPF("application/vnd.yamaha.smaf-phrase", "spf"), + SPL("application/x-futuresplash", "spl"), + SPOT("text/vnd.in3d.spot", "spot"), + SPP("application/scvp-vp-response", "spp"), + SPQ("application/scvp-vp-request", "spq"), + SPX("audio/ogg", "spx"), + SRC("application/x-wais-source", "src"), + SRX("application/sparql-results+xml", "srx"), + SSE("application/vnd.kodak-descriptor", "sse"), + SSF("application/vnd.epson.ssf", "ssf"), + SSML("application/ssml+xml", "ssml"), + STC("application/vnd.sun.xml.calc.template", "stc"), + STD("application/vnd.sun.xml.draw.template", "std"), + STF("application/vnd.wt.stf", "stf"), + STI("application/vnd.sun.xml.impress.template", "sti"), + STK("application/hyperstudio", "stk"), + STL("application/vnd.ms-pki.stl", "stl"), + STR("application/vnd.pg.format", "str"), + STW("application/vnd.sun.xml.writer.template", "stw"), + SUS("application/vnd.sus-calendar", "sus"), + SUSP("application/vnd.sus-calendar", "susp"), + SV4CPIO("application/x-sv4cpio", "sv4cpio"), + SV4CRC("application/x-sv4crc", "sv4crc"), + SVD("application/vnd.svd", "svd"), + SVG("image/svg+xml", "svg"), + SVGZ("image/svg+xml", "svgz"), + SWA("application/x-director", "swa"), + SWF("application/x-shockwave-flash", "swf"), + SWI("application/vnd.arastra.swi", "swi"), + SXC("application/vnd.sun.xml.calc", "sxc"), + SXD("application/vnd.sun.xml.draw", "sxd"), + SXG("application/vnd.sun.xml.writer.global", "sxg"), + SXI("application/vnd.sun.xml.impress", "sxi"), + SXM("application/vnd.sun.xml.math", "sxm"), + SXW("application/vnd.sun.xml.writer", "sxw"), + T("text/troff", "t"), + TAO("application/vnd.tao.intent-module-archive", "tao"), + TAR("application/x-tar", "tar"), + TCAP("application/vnd.3gpp2.tcap", "tcap"), + TCL("application/x-tcl", "tcl"), + TEACHER("application/vnd.smart.teacher", "teacher"), + TEX("application/x-tex", "tex"), + TEXI("application/x-texinfo", "texi"), + TEXINFO("application/x-texinfo", "texinfo"), + TEXT("text/plain", "text"), + TFM("application/x-tex-tfm", "tfm"), + TGZ("application/x-gzip", "tgz"), + TIF("image/tiff", "tif"), + TIFF("image/tiff", "tiff"), + TMO("application/vnd.tmobile-livetv", "tmo"), + TORRENT("application/x-bittorrent", "torrent"), + TPL("application/vnd.groove-tool-template", "tpl"), + TPT("application/vnd.trid.tpt", "tpt"), + TR("text/troff", "tr"), + TRA("application/vnd.trueapp", "tra"), + TRM("application/x-msterminal", "trm"), + TSV("text/tab-separated-values", "tsv"), + TTC("application/x-font-ttf", "ttc"), + TTF("application/x-font-ttf", "ttf"), + TWD("application/vnd.simtech-mindmapper", "twd"), + TWDS("application/vnd.simtech-mindmapper", "twds"), + TXD("application/vnd.genomatix.tuxedo", "txd"), + TXF("application/vnd.mobius.txf", "txf"), + TXT("text/plain", "txt"), + U32("application/x-authorware-bin", "u32"), + UDEB("application/x-debian-package", "udeb"), + UFD("application/vnd.ufdl", "ufd"), + UFDL("application/vnd.ufdl", "ufdl"), + UMJ("application/vnd.umajin", "umj"), + UNITYWEB("application/vnd.unity", "unityweb"), + UOML("application/vnd.uoml+xml", "uoml"), + URI("text/uri-list", "uri"), + URIS("text/uri-list", "uris"), + URLS("text/uri-list", "urls"), + USTAR("application/x-ustar", "ustar"), + UTZ("application/vnd.uiq.theme", "utz"), + UU("text/x-uuencode", "uu"), + VCD("application/x-cdlink", "vcd"), + VCF("text/x-vcard", "vcf"), + VCG("application/vnd.groove-vcard", "vcg"), + VCS("text/x-vcalendar", "vcs"), + VCX("application/vnd.vcx", "vcx"), + VIS("application/vnd.visionary", "vis"), + VIV("video/vnd.vivo", "viv"), + VOR("application/vnd.stardivision.writer", "vor"), + VOX("application/x-authorware-bin", "vox"), + VRML("model/vrml", "vrml"), + VSD("application/vnd.visio", "vsd"), + VSF("application/vnd.vsf", "vsf"), + VSS("application/vnd.visio", "vss"), + VST("application/vnd.visio", "vst"), + VSW("application/vnd.visio", "vsw"), + VTU("model/vnd.vtu", "vtu"), + VXML("application/voicexml+xml", "vxml"), + W3D("application/x-director", "w3d"), + WAD("application/x-doom", "wad"), + WAV("audio/x-wav", "wav"), + WAX("audio/x-ms-wax", "wax"), + WBMP("image/vnd.wap.wbmp", "wbmp"), + WBS("application/vnd.criticaltools.wbs+xml", "wbs"), + WBXML("application/vnd.wap.wbxml", "wbxml"), + WCM("application/vnd.ms-works", "wcm"), + WDB("application/vnd.ms-works", "wdb"), + WIZ("application/msword", "wiz"), + WKS("application/vnd.ms-works", "wks"), + WM("video/x-ms-wm", "wm"), + WMA("audio/x-ms-wma", "wma"), + WMD("application/x-ms-wmd", "wmd"), + WMF("application/x-msmetafile", "wmf"), + WML("text/vnd.wap.wml", "wml"), + WMLC("application/vnd.wap.wmlc", "wmlc"), + WMLS("text/vnd.wap.wmlscript", "wmls"), + WMLSC("application/vnd.wap.wmlscriptc", "wmlsc"), + WMV("video/x-ms-wmv", "wmv"), + WMX("video/x-ms-wmx", "wmx"), + WMZ("application/x-ms-wmz", "wmz"), + WPD("application/vnd.wordperfect", "wpd"), + WPL("application/vnd.ms-wpl", "wpl"), + WPS("application/vnd.ms-works", "wps"), + WQD("application/vnd.wqd", "wqd"), + WRI("application/x-mswrite", "wri"), + WRL("model/vrml", "wrl"), + WSDL("application/wsdl+xml", "wsdl"), + WSPOLICY("application/wspolicy+xml", "wspolicy"), + WTB("application/vnd.webturbo", "wtb"), + WVX("video/x-ms-wvx", "wvx"), + X32("application/x-authorware-bin", "x32"), + X3D("application/vnd.hzn-3d-crossword", "x3d"), + XAP("application/x-silverlight-app", "xap"), + XAR("application/vnd.xara", "xar"), + XBAP("application/x-ms-xbap", "xbap"), + XBD("application/vnd.fujixerox.docuworks.binder", "xbd"), + XBM("image/x-xbitmap", "xbm"), + XDM("application/vnd.syncml.dm+xml", "xdm"), + XDP("application/vnd.adobe.xdp+xml", "xdp"), + XDW("application/vnd.fujixerox.docuworks", "xdw"), + XENC("application/xenc+xml", "xenc"), + XER("application/patch-ops-error+xml", "xer"), + XFDF("application/vnd.adobe.xfdf", "xfdf"), + XFDL("application/vnd.xfdl", "xfdl"), + XHT("application/xhtml+xml", "xht"), + XHTML("application/xhtml+xml", "xhtml"), + XHVML("application/xv+xml", "xhvml"), + XIF("image/vnd.xiff", "xif"), + XLA("application/vnd.ms-excel", "xla"), + XLAM("application/vnd.ms-excel.addin.macroenabled.12", "xlam"), + XLB("application/vnd.ms-excel", "xlb"), + XLC("application/vnd.ms-excel", "xlc"), + XLM("application/vnd.ms-excel", "xlm"), + XLS("application/vnd.ms-excel", "xls"), + XLSB("application/vnd.ms-excel.sheet.binary.macroenabled.12", "xlsb"), + XLSM("application/vnd.ms-excel.sheet.macroenabled.12", "xlsm"), + XLSX("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx"), + XLT("application/vnd.ms-excel", "xlt"), + XLTM("application/vnd.ms-excel.template.macroenabled.12", "xltm"), + XLTX("application/vnd.openxmlformats-officedocument.spreadsheetml.template", "xltx"), + XLW("application/vnd.ms-excel", "xlw"), + XML("application/xml", "xml"), + XO("application/vnd.olpc-sugar", "xo"), + XOP("application/xop+xml", "xop"), + XPDL("application/xml", "xpdl"), + XPI("application/x-xpinstall", "xpi"), + XPM("image/x-xpixmap", "xpm"), + XPR("application/vnd.is-xpr", "xpr"), + XPS("application/vnd.ms-xpsdocument", "xps"), + XPW("application/vnd.intercon.formnet", "xpw"), + XPX("application/vnd.intercon.formnet", "xpx"), + XSL("application/xml", "xsl"), + XSLT("application/xslt+xml", "xslt"), + XSM("application/vnd.syncml+xml", "xsm"), + XSPF("application/xspf+xml", "xspf"), + XUL("application/vnd.mozilla.xul+xml", "xul"), + XVM("application/xv+xml", "xvm"), + XVML("application/xv+xml", "xvml"), + XWD("image/x-xwindowdump", "xwd"), + XYZ("chemical/x-xyz", "xyz"), + ZAZ("application/vnd.zzazz.deck+xml", "zaz"), + ZIP("application/zip", "zip"), + ZIR("application/vnd.zul", "zir"), + ZIRZ("application/vnd.zul", "zirz"), + ZMM("application/vnd.handheld-entertainment+xml", "zmm"); + + private final String headerValue; + private final List fileExtensions; + + // Static map for quick lookup + private static final Map lookup = new HashMap<>(); + + // Populate the lookup map when the class is loaded + static { + for (ContentType contentType : ContentType.values()) { + for (String fileExtension : contentType.fileExtensions) { + lookup.put(fileExtension, contentType); + } + } + } + + ContentType(String headerValue, String... fileExtensions) { + this.headerValue = headerValue; + this.fileExtensions = Arrays.asList(fileExtensions); + } + + public String getHeaderValue() { + return headerValue; + } + + public List getFileExtensions() { + return fileExtensions; + } + + public static ContentType fromFileExtension(String fileExtension) { + return lookup.get(fileExtension); + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Header.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Header.java new file mode 100755 index 0000000..652b3aa --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Header.java @@ -0,0 +1,58 @@ +package net.jonathangiles.tools.teenyhttpd.model; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Represents a request header. + */ +public class Header { + private final String keyValue; + private String key; + private List values; + + public Header(final String keyValue) { + this.keyValue = keyValue; + } + + public Header(final String key, final String value) { + this(key, Collections.singletonList(value)); + } + + public Header(final String key, final List values) { + this.keyValue = null; + this.key = key; + this.values = values; + } + + public String getKey() { + if (key == null) { + parse(); + } + return key; + } + + public List getValues() { + if (values == null) { + parse(); + } + return values; + } + + @Override + public String toString() { + return getKey() + ": " + String.join(", ", getValues()); + } + + private void parse() { + if (keyValue == null || keyValue.isEmpty()) { + throw new IllegalArgumentException("keyValue must not be null or empty"); + } + + final String[] split = keyValue.split(":", 2); + key = split[0].trim(); + values = Arrays.asList(split[1].split(",")); + values.replaceAll(String::trim); + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Headers.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Headers.java new file mode 100644 index 0000000..11af0ad --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Headers.java @@ -0,0 +1,24 @@ +package net.jonathangiles.tools.teenyhttpd.model; + +public enum Headers { + ACCEPT("Accept"), + ACCESS_CONTROL_ALLOW_ORIGIN("Access-Control-Allow-Origin"), + CACHE_CONTROL("Cache-Control"), + CONNECTION("Connection"), + CONTENT_TYPE("Content-Type"), + CONTENT_LENGTH("Content-Length"); + + private final String key; + + Headers(final String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public Header asHeader(final String value) { + return new Header(key, value); + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/request/Method.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Method.java similarity index 84% rename from src/main/java/net/jonathangiles/tools/teenyhttpd/request/Method.java rename to src/main/java/net/jonathangiles/tools/teenyhttpd/model/Method.java index f0ec567..32c73f0 100755 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/request/Method.java +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Method.java @@ -1,4 +1,4 @@ -package net.jonathangiles.tools.teenyhttpd.request; +package net.jonathangiles.tools.teenyhttpd.model; /** * An enumeration listing all of the available request methods that are possible. Note that note all of these request diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/request/QueryParams.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/QueryParams.java similarity index 91% rename from src/main/java/net/jonathangiles/tools/teenyhttpd/request/QueryParams.java rename to src/main/java/net/jonathangiles/tools/teenyhttpd/model/QueryParams.java index ec2beb9..eb2f1e0 100755 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/request/QueryParams.java +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/QueryParams.java @@ -1,4 +1,4 @@ -package net.jonathangiles.tools.teenyhttpd.request; +package net.jonathangiles.tools.teenyhttpd.model; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; @@ -21,6 +21,11 @@ public QueryParams(final String allParams) { this.allParams = allParams; } + public QueryParams(final Map queryParams) { + this.allParams = null; + this.map = queryParams; + } + /** * Returns all query params parsed into a Map. */ diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Request.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Request.java new file mode 100755 index 0000000..f982693 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Request.java @@ -0,0 +1,43 @@ +package net.jonathangiles.tools.teenyhttpd.model; + +import net.jonathangiles.tools.teenyhttpd.implementation.SimpleRequest; + +import java.util.*; + +/** + * Represents an incoming request. + */ +public interface Request { + + static Request create(final Method method, final String path, final QueryParams queryParams) { + return new SimpleRequest(method, path, queryParams); + } + + static Request create(Method method, String path, QueryParams queryParams, List
headers, Map pathParamsMap) { + return new SimpleRequest(method, path, queryParams, headers, pathParamsMap); + } + + static Request create(Method method, String path, QueryParams queryParams, Map headers, Map pathParamsMap) { + return new SimpleRequest(method, path, queryParams, headers, pathParamsMap); + } + + Method getMethod(); + + String getPath(); + + /** + * Returns a read-only Map of headers. + * @return + */ + Map getHeaders(); + + Optional
getHeader(final String header); + + Optional
getHeader(final Headers header); + + Map getQueryParams(); + + Map getPathParams(); +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Response.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Response.java new file mode 100755 index 0000000..f3d2897 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/Response.java @@ -0,0 +1,60 @@ +package net.jonathangiles.tools.teenyhttpd.model; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.List; + +import net.jonathangiles.tools.teenyhttpd.implementation.ByteResponse; +import net.jonathangiles.tools.teenyhttpd.implementation.EmptyResponse; +import net.jonathangiles.tools.teenyhttpd.implementation.FileResponse; +import net.jonathangiles.tools.teenyhttpd.implementation.StringResponse; + +public interface Response { + + static Response create(final StatusCode statusCode) { + return new EmptyResponse(statusCode); + } + + static Response create(final StatusCode statusCode, final List
headers) { + return new EmptyResponse(statusCode, headers); + } + + static Response create(final StatusCode statusCode, final byte[] body) { + return new ByteResponse(statusCode, body); + } + + static Response create(final StatusCode statusCode, final List
headers, final byte[] body) { + return new ByteResponse(statusCode, headers, body); + } + + static Response create(final String body) { + return new StringResponse(body); + } + + static Response create(final StatusCode statusCode, final String body) { + return new StringResponse(statusCode, body); + } + + static Response create(final StatusCode statusCode, final List
headers, final String body) { + return new StringResponse(statusCode, headers, body); + } + + static Response createFileResponse(final Request request) { + return new FileResponse(request); + } + + StatusCode getStatusCode(); + + default List
getHeaders() { + return Collections.emptyList(); + } + + void setHeader(Header header); + + default long getBodyLength() { + return 0; + } + + void writeBody(BufferedOutputStream dataOut) throws IOException; +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/model/ServerSentEventHandler.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/ServerSentEventHandler.java new file mode 100644 index 0000000..9847e64 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/ServerSentEventHandler.java @@ -0,0 +1,92 @@ +package net.jonathangiles.tools.teenyhttpd.model; + +import net.jonathangiles.tools.teenyhttpd.implementation.ServerSentEventHandlerImpl; +import net.jonathangiles.tools.teenyhttpd.implementation.ServerSentEventRequest; + +import java.util.function.Consumer; +import java.util.function.Function; + +public interface ServerSentEventHandler { + + static ServerSentEventHandler create() { + return create(null); + } + + static ServerSentEventHandler create(Consumer onActive) { + return create(onActive, null); + } + + static ServerSentEventHandler create(Consumer onActive, + Consumer onInactive) { + return new ServerSentEventHandlerImpl() { + @Override public void onActive() { + if (onActive != null) { + onActive.accept(this); + } + } + + @Override + public void onInactive() { + if (onInactive != null) { + onInactive.accept(this); + } + } + }; + } + + /** + * Called when the client connects to the SSE endpoint. + * @param request The initial request from the client. + */ + void onConnect(Request request); + + /** + * Called when the client disconnects from the SSE endpoint. + * @param request The initial request from the client. + */ + void onDisconnect(Request request); + + /** + * Sends a message to all clients, with the message appended to the `data` field of the Server-Sent Event. + * @param message The message to send. + */ + default void sendMessage(String message) { + sendMessage(new ServerSentEventMessage(message)); + } + + /** + * Sends a message to all clients, with each field of the {@link ServerSentEventMessage} being set in the message, + * as long as the field is not null. + * @param message The message to send. + */ + default void sendMessage(ServerSentEventMessage message) { + sendMessage(request -> message); + } + + /** + * Sends a message to all clients, with the message being generated by the provided function. This allows for the + * message to be tailored per client, if required (e.g. by using path parameters or query parameters). + * + * @param messageGenerator A function that takes a {@link ServerSentEventRequest} and returns a + * {@link ServerSentEventMessage}, that will then be sent to the client represented by the given request. + */ + void sendMessage(final Function messageGenerator); + + /** + * Returns true if there are active connections to the SSE endpoint. + * @return true if there are active connections to the SSE endpoint. + */ + boolean hasActiveConnections(); + + /** + * Called when the first client connects to the SSE endpoint. This is a good point to start any background threads + * that will generate data, which can then be sent by calling the sendMessage methods. + */ + void onActive(); + + /** + * Called when the last client disconnects from the SSE endpoint. This is a good point to stop any background threads + * that are generating data. + */ + void onInactive(); +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/model/ServerSentEventMessage.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/ServerSentEventMessage.java new file mode 100644 index 0000000..92527b1 --- /dev/null +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/ServerSentEventMessage.java @@ -0,0 +1,69 @@ +package net.jonathangiles.tools.teenyhttpd.model; + +import java.time.Duration; + +public class ServerSentEventMessage { + private final String message; + private final String event; + private final String id; + private final Duration retry; + private final String comment; + + public ServerSentEventMessage(String message) { + this.message = message; + this.event = null; + this.id = null; + this.retry = null; + this.comment = null; + } + + public ServerSentEventMessage(String message, String event) { + this.message = message; + this.event = event; + this.id = null; + this.retry = null; + this.comment = null; + } + + public ServerSentEventMessage(String message, String event, String id) { + this.message = message; + this.event = event; + this.id = id; + this.retry = null; + this.comment = null; + } + + public ServerSentEventMessage(String message, String event, String id, Duration retry, String comment) { + this.message = message; + this.event = event; + this.id = id; + this.retry = retry; + this.comment = comment; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + if (comment != null) { + sb.append(": ").append(comment).append("\n"); + } + if (event != null) { + sb.append("event: ").append(event).append("\n"); + } + if (id != null) { + sb.append("id: ").append(id).append("\n"); + } + if (message != null) { + String[] lines = message.split("\n"); + for (String line : lines) { + sb.append("data: ").append(line).append("\n"); + } + } + if (retry != null) { + sb.append("retry: ").append(retry.toMillis()).append("\n"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/StatusCode.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/StatusCode.java similarity index 94% rename from src/main/java/net/jonathangiles/tools/teenyhttpd/response/StatusCode.java rename to src/main/java/net/jonathangiles/tools/teenyhttpd/model/StatusCode.java index 5111694..1012cb2 100755 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/StatusCode.java +++ b/src/main/java/net/jonathangiles/tools/teenyhttpd/model/StatusCode.java @@ -1,4 +1,6 @@ -package net.jonathangiles.tools.teenyhttpd.response; +package net.jonathangiles.tools.teenyhttpd.model; + +import net.jonathangiles.tools.teenyhttpd.implementation.EmptyResponse; public enum StatusCode { // https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/request/Header.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/request/Header.java deleted file mode 100755 index 1b931dc..0000000 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/request/Header.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.jonathangiles.tools.teenyhttpd.request; - -/** - * Represents a request header. - */ -public class Header { - private final String keyValue; - private String key; - private String value; - - public Header(final String keyValue) { - this.keyValue = keyValue; - } - - public String getKey() { - if (key == null) { - parse(); - } - return key; - } - - public String getValue() { - if (value == null) { - parse(); - } - return value; - } - - @Override - public String toString() { - return getKey() + ": " + getValue(); - } - - private void parse() { - final String[] split = keyValue.split(":", 2); - key = split[0].trim(); - value = split[1].trim(); - } -} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/request/Request.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/request/Request.java deleted file mode 100755 index 9a99f20..0000000 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/request/Request.java +++ /dev/null @@ -1,69 +0,0 @@ -package net.jonathangiles.tools.teenyhttpd.request; - -import java.util.*; - -/** - * Represents an incoming request. - */ -public class Request { - private final Method method; - private final String path; - private final QueryParams queryParams; - private List
headers; - private Map headersMap; - - private Map pathParams = new HashMap<>(); // FIXME: this is a hack - - public Request(final Method method, final String path, final QueryParams queryParams) { - this.method = method; - this.path = path; - this.queryParams = queryParams; - } - - public Method getMethod() { - return method; - } - - public String getPath() { - return path; - } - - public void addHeader(final Header header) { - if (headers == null) { - headers = new ArrayList<>(); - } - headers.add(header); - } - - public Map getHeaders() { - if (headersMap == null) { - headersMap = new LinkedHashMap<>(); - headers.forEach(header -> { - headersMap.put(header.getKey(), header.getValue()); - }); - } - return headersMap; - } - - public Map getQueryParams() { - return queryParams.getQueryParams(); - } - - @Override - public String toString() { - return "Request{" + - "method=" + method + - ", path='" + path + '\'' + - ", queryParams=" + queryParams + - ", headers=" + headers + - '}'; - } - - public Map getPathParams() { - return pathParams; - } - - public void addPathParam(String name, String value) { - pathParams.put(name, value); - } -} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/ByteResponse.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/response/ByteResponse.java deleted file mode 100755 index 399a5b3..0000000 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/ByteResponse.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.jonathangiles.tools.teenyhttpd.response; - -import net.jonathangiles.tools.teenyhttpd.request.Request; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -public class ByteResponse implements Response { - final StatusCode statusCode; - final List headers; - final byte[] body; - - public ByteResponse(final StatusCode statusCode) { - this(statusCode, Collections.emptyList()); - } - - public ByteResponse(final StatusCode statusCode, final List headers) { - this(statusCode, headers, null); - } - - public ByteResponse(final StatusCode statusCode, final byte[] body) { - this(statusCode, Collections.emptyList(), body); - } - - public ByteResponse(final StatusCode statusCode, final List headers, final byte[] body) { - this.statusCode = statusCode; - this.headers = headers; - this.body = body; - } - - @Override - public StatusCode getStatusCode() { - return statusCode; - } - - @Override - public List getHeaders() { - return headers; - } - - @Override - public long getBodyLength() { - return body == null ? 0 : body.length; - } - - @Override - public void writeBody(BufferedOutputStream dataOut) throws IOException { - if (body != null) { - dataOut.write(body, 0, body.length); - dataOut.flush(); - } - } -} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/EmptyResponse.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/response/EmptyResponse.java deleted file mode 100755 index 684f37a..0000000 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/EmptyResponse.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.jonathangiles.tools.teenyhttpd.response; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -class EmptyResponse implements Response { - final StatusCode statusCode; - - public EmptyResponse(final StatusCode statusCode) { - this.statusCode = statusCode; - } - - @Override - public StatusCode getStatusCode() { - return statusCode; - } - - @Override - public List getHeaders() { - return Collections.emptyList(); - } - - @Override - public void writeBody(BufferedOutputStream dataOut) throws IOException { - // no-op - } -} diff --git a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/Response.java b/src/main/java/net/jonathangiles/tools/teenyhttpd/response/Response.java deleted file mode 100755 index 55c755f..0000000 --- a/src/main/java/net/jonathangiles/tools/teenyhttpd/response/Response.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.jonathangiles.tools.teenyhttpd.response; - -import net.jonathangiles.tools.teenyhttpd.request.Request; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.List; - -public interface Response { - - StatusCode getStatusCode(); - - default List getHeaders() { - return Collections.emptyList(); - } - - default long getBodyLength() { - return 0; - } - - void writeBody(BufferedOutputStream dataOut) throws IOException; - -} diff --git a/src/main/resources/net/jonathangiles/tools/teenyhttpd/response/contentTypes.properties b/src/main/resources/net/jonathangiles/tools/teenyhttpd/response/contentTypes.properties deleted file mode 100755 index 8bafb76..0000000 --- a/src/main/resources/net/jonathangiles/tools/teenyhttpd/response/contentTypes.properties +++ /dev/null @@ -1,817 +0,0 @@ -123=application/vnd.lotus-1-2-3 -3dml=text/vnd.in3d.3dml -3g2=video/3gpp2 -3gp=video/3gpp -a=application/octet-stream -aab=application/x-authorware-bin -aac=audio/x-aac -aam=application/x-authorware-map -aas=application/x-authorware-seg -abw=application/x-abiword -acc=application/vnd.americandynamics.acc -ace=application/x-ace-compressed -acu=application/vnd.acucobol -acutc=application/vnd.acucorp -adp=audio/adpcm -aep=application/vnd.audiograph -afm=application/x-font-type1 -afp=application/vnd.ibm.modcap -ai=application/postscript -aif=audio/x-aiff -aifc=audio/x-aiff -aiff=audio/x-aiff -air=application/vnd.adobe.air-application-installer-package+zip -ami=application/vnd.amiga.ami -apk=application/vnd.android.package-archive -application=application/x-ms-application -apr=application/vnd.lotus-approach -asc=application/pgp-signature -asf=video/x-ms-asf -asm=text/x-asm -aso=application/vnd.accpac.simply.aso -asx=video/x-ms-asf -atc=application/vnd.acucorp -atom=application/atom+xml -atomcat=application/atomcat+xml -atomsvc=application/atomsvc+xml -atx=application/vnd.antix.game-component -au=audio/basic -avi=video/x-msvideo -aw=application/applixware -azf=application/vnd.airzip.filesecure.azf -azs=application/vnd.airzip.filesecure.azs -azw=application/vnd.amazon.ebook -bat=application/x-msdownload -bcpio=application/x-bcpio -bdf=application/x-font-bdf -bdm=application/vnd.syncml.dm+wbxml -bh2=application/vnd.fujitsu.oasysprs -bin=application/octet-stream -bmi=application/vnd.bmi -bmp=image/bmp -book=application/vnd.framemaker -box=application/vnd.previewsystems.box -boz=application/x-bzip2 -bpk=application/octet-stream -btif=image/prs.btif -bz=application/x-bzip -bz2=application/x-bzip2 -c=text/x-c -c4d=application/vnd.clonk.c4group -c4f=application/vnd.clonk.c4group -c4g=application/vnd.clonk.c4group -c4p=application/vnd.clonk.c4group -c4u=application/vnd.clonk.c4group -cab=application/vnd.ms-cab-compressed -car=application/vnd.curl.car -cat=application/vnd.ms-pki.seccat -cc=text/x-c -cct=application/x-director -ccxml=application/ccxml+xml -cdbcmsg=application/vnd.contact.cmsg -cdf=application/x-netcdf -cdkey=application/vnd.mediastation.cdkey -cdx=chemical/x-cdx -cdxml=application/vnd.chemdraw+xml -cdy=application/vnd.cinderella -cer=application/pkix-cert -cgm=image/cgm -chat=application/x-chat -chm=application/vnd.ms-htmlhelp -chrt=application/vnd.kde.kchart -cif=chemical/x-cif -cii=application/vnd.anser-web-certificate-issue-initiation -cil=application/vnd.ms-artgalry -cla=application/vnd.claymore -class=application/java-vm -clkk=application/vnd.crick.clicker.keyboard -clkp=application/vnd.crick.clicker.palette -clkt=application/vnd.crick.clicker.template -clkw=application/vnd.crick.clicker.wordbank -clkx=application/vnd.crick.clicker -clp=application/x-msclip -cmc=application/vnd.cosmocaller -cmdf=chemical/x-cmdf -cml=chemical/x-cml -cmp=application/vnd.yellowriver-custom-menu -cmx=image/x-cmx -cod=application/vnd.rim.cod -com=application/x-msdownload -conf=text/plain -cpio=application/x-cpio -cpp=text/x-c -cpt=application/mac-compactpro -crd=application/x-mscardfile -crl=application/pkix-crl -crt=application/x-x509-ca-cert -csh=application/x-csh -csml=chemical/x-csml -csp=application/vnd.commonspace -css=text/css -cst=application/x-director -csv=text/csv -cu=application/cu-seeme -curl=text/vnd.curl -cww=application/prs.cww -cxt=application/x-director -cxx=text/x-c -daf=application/vnd.mobius.daf -dataless=application/vnd.fdsn.seed -davmount=application/davmount+xml -dcr=application/x-director -dcurl=text/vnd.curl.dcurl -dd2=application/vnd.oma.dd2+xml -ddd=application/vnd.fujixerox.ddd -deb=application/x-debian-package -def=text/plain -deploy=application/octet-stream -der=application/x-x509-ca-cert -dfac=application/vnd.dreamfactory -dic=text/x-c -diff=text/plain -dir=application/x-director -dis=application/vnd.mobius.dis -dist=application/octet-stream -distz=application/octet-stream -djv=image/vnd.djvu -djvu=image/vnd.djvu -dll=application/x-msdownload -dmg=application/octet-stream -dms=application/octet-stream -dna=application/vnd.dna -doc=application/msword -docm=application/vnd.ms-word.document.macroenabled.12 -docx=application/vnd.openxmlformats-officedocument.wordprocessingml.document -dot=application/msword -dotm=application/vnd.ms-word.template.macroenabled.12 -dotx=application/vnd.openxmlformats-officedocument.wordprocessingml.template -dp=application/vnd.osgi.dp -dpg=application/vnd.dpgraph -dsc=text/prs.lines.tag -dtb=application/x-dtbook+xml -dtd=application/xml-dtd -dts=audio/vnd.dts -dtshd=audio/vnd.dts.hd -dump=application/octet-stream -dvi=application/x-dvi -dwf=model/vnd.dwf -dwg=image/vnd.dwg -dxf=image/vnd.dxf -dxp=application/vnd.spotfire.dxp -dxr=application/x-director -ecelp4800=audio/vnd.nuera.ecelp4800 -ecelp7470=audio/vnd.nuera.ecelp7470 -ecelp9600=audio/vnd.nuera.ecelp9600 -ecma=application/ecmascript -edm=application/vnd.novadigm.edm -edx=application/vnd.novadigm.edx -efif=application/vnd.picsel -ei6=application/vnd.pg.osasli -elc=application/octet-stream -eml=message/rfc822 -emma=application/emma+xml -eol=audio/vnd.digital-winds -eot=application/vnd.ms-fontobject -eps=application/postscript -epub=application/epub+zip -es3=application/vnd.eszigno3+xml -esf=application/vnd.epson.esf -et3=application/vnd.eszigno3+xml -etx=text/x-setext -exe=application/x-msdownload -ext=application/vnd.novadigm.ext -ez=application/andrew-inset -ez2=application/vnd.ezpix-album -ez3=application/vnd.ezpix-package -f=text/x-fortran -f4v=video/x-f4v -f77=text/x-fortran -f90=text/x-fortran -fbs=image/vnd.fastbidsheet -fdf=application/vnd.fdf -fe_launch=application/vnd.denovo.fcselayout-link -fg5=application/vnd.fujitsu.oasysgp -fgd=application/x-director -fh=image/x-freehand -fh4=image/x-freehand -fh5=image/x-freehand -fh7=image/x-freehand -fhc=image/x-freehand -fig=application/x-xfig -fli=video/x-fli -flo=application/vnd.micrografx.flo -flv=video/x-flv -flw=application/vnd.kde.kivio -flx=text/vnd.fmi.flexstor -fly=text/vnd.fly -fm=application/vnd.framemaker -fnc=application/vnd.frogans.fnc -for=text/x-fortran -fpx=image/vnd.fpx -frame=application/vnd.framemaker -fsc=application/vnd.fsc.weblaunch -fst=image/vnd.fst -ftc=application/vnd.fluxtime.clip -fti=application/vnd.anser-web-funds-transfer-initiation -fvt=video/vnd.fvt -fzs=application/vnd.fuzzysheet -g3=image/g3fax -gac=application/vnd.groove-account -gdl=model/vnd.gdl -geo=application/vnd.dynageo -gex=application/vnd.geometry-explorer -ggb=application/vnd.geogebra.file -ggt=application/vnd.geogebra.tool -ghf=application/vnd.groove-help -gif=image/gif -gim=application/vnd.groove-identity-message -gmx=application/vnd.gmx -gnumeric=application/x-gnumeric -gph=application/vnd.flographit -gqf=application/vnd.grafeq -gqs=application/vnd.grafeq -gram=application/srgs -gre=application/vnd.geometry-explorer -grv=application/vnd.groove-injector -grxml=application/srgs+xml -gsf=application/x-font-ghostscript -gtar=application/x-gtar -gtm=application/vnd.groove-tool-message -gtw=model/vnd.gtw -gv=text/vnd.graphviz -gz=application/x-gzip -h=text/x-c -h261=video/h261 -h263=video/h263 -h264=video/h264 -hbci=application/vnd.hbci -hdf=application/x-hdf -hh=text/x-c -hlp=application/winhlp -hpgl=application/vnd.hp-hpgl -hpid=application/vnd.hp-hpid -hps=application/vnd.hp-hps -hqx=application/mac-binhex40 -htke=application/vnd.kenameaapp -htm=text/html -html=text/html -hvd=application/vnd.yamaha.hv-dic -hvp=application/vnd.yamaha.hv-voice -hvs=application/vnd.yamaha.hv-script -icc=application/vnd.iccprofile -ice=x-conference/x-cooltalk -icm=application/vnd.iccprofile -ico=image/x-icon -ics=text/calendar -ief=image/ief -ifb=text/calendar -ifm=application/vnd.shana.informed.formdata -iges=model/iges -igl=application/vnd.igloader -igs=model/iges -igx=application/vnd.micrografx.igx -iif=application/vnd.shana.informed.interchange -imp=application/vnd.accpac.simply.imp -ims=application/vnd.ms-ims -in=text/plain -ipk=application/vnd.shana.informed.package -irm=application/vnd.ibm.rights-management -irp=application/vnd.irepository.package+xml -iso=application/octet-stream -itp=application/vnd.shana.informed.formtemplate -ivp=application/vnd.immervision-ivp -ivu=application/vnd.immervision-ivu -jad=text/vnd.sun.j2me.app-descriptor -jam=application/vnd.jam -jar=application/java-archive -java=text/x-java-source -jisp=application/vnd.jisp -jlt=application/vnd.hp-jlyt -jnlp=application/x-java-jnlp-file -joda=application/vnd.joost.joda-archive -jpe=image/jpeg -jpeg=image/jpeg -jpg=image/jpeg -jpgm=video/jpm -jpgv=video/jpeg -jpm=video/jpm -js=application/javascript -json=application/json -kar=audio/midi -karbon=application/vnd.kde.karbon -kfo=application/vnd.kde.kformula -kia=application/vnd.kidspiration -kil=application/x-killustrator -kml=application/vnd.google-earth.kml+xml -kmz=application/vnd.google-earth.kmz -kne=application/vnd.kinar -knp=application/vnd.kinar -kon=application/vnd.kde.kontour -kpr=application/vnd.kde.kpresenter -kpt=application/vnd.kde.kpresenter -ksh=text/plain -ksp=application/vnd.kde.kspread -ktr=application/vnd.kahootz -ktz=application/vnd.kahootz -kwd=application/vnd.kde.kword -kwt=application/vnd.kde.kword -latex=application/x-latex -lbd=application/vnd.llamagraphics.life-balance.desktop -lbe=application/vnd.llamagraphics.life-balance.exchange+xml -les=application/vnd.hhe.lesson-player -lha=application/octet-stream -link66=application/vnd.route66.link66+xml -list=text/plain -list3820=application/vnd.ibm.modcap -listafp=application/vnd.ibm.modcap -log=text/plain -lostxml=application/lost+xml -lrf=application/octet-stream -lrm=application/vnd.ms-lrm -ltf=application/vnd.frogans.ltf -lvp=audio/vnd.lucent.voice -lwp=application/vnd.lotus-wordpro -lzh=application/octet-stream -m13=application/x-msmediaview -m14=application/x-msmediaview -m1v=video/mpeg -m2a=audio/mpeg -m2v=video/mpeg -m3a=audio/mpeg -m3u=audio/x-mpegurl -m4u=video/vnd.mpegurl -m4v=video/x-m4v -ma=application/mathematica -mag=application/vnd.ecowin.chart -maker=application/vnd.framemaker -man=text/troff -mathml=application/mathml+xml -mb=application/mathematica -mbk=application/vnd.mobius.mbk -mbox=application/mbox -mc1=application/vnd.medcalcdata -mcd=application/vnd.mcd -mcurl=text/vnd.curl.mcurl -mdb=application/x-msaccess -mdi=image/vnd.ms-modi -me=text/troff -mesh=model/mesh -mfm=application/vnd.mfmp -mgz=application/vnd.proteus.magazine -mht=message/rfc822 -mhtml=message/rfc822 -mid=audio/midi -midi=audio/midi -mif=application/vnd.mif -mime=message/rfc822 -mj2=video/mj2 -mjp2=video/mj2 -mlp=application/vnd.dolby.mlp -mmd=application/vnd.chipnuts.karaoke-mmd -mmf=application/vnd.smaf -mmr=image/vnd.fujixerox.edmics-mmr -mny=application/x-msmoney -mobi=application/x-mobipocket-ebook -mov=video/quicktime -movie=video/x-sgi-movie -mp2=audio/mpeg -mp2a=audio/mpeg -mp3=audio/mpeg -mp4=video/mp4 -mp4a=audio/mp4 -mp4s=application/mp4 -mp4v=video/mp4 -mpa=video/mpeg -mpc=application/vnd.mophun.certificate -mpe=video/mpeg -mpeg=video/mpeg -mpg=video/mpeg -mpg4=video/mp4 -mpga=audio/mpeg -mpkg=application/vnd.apple.installer+xml -mpm=application/vnd.blueice.multipass -mpn=application/vnd.mophun.application -mpp=application/vnd.ms-project -mpt=application/vnd.ms-project -mpy=application/vnd.ibm.minipay -mqy=application/vnd.mobius.mqy -mrc=application/marc -ms=text/troff -mscml=application/mediaservercontrol+xml -mseed=application/vnd.fdsn.mseed -mseq=application/vnd.mseq -msf=application/vnd.epson.msf -msh=model/mesh -msi=application/x-msdownload -msl=application/vnd.mobius.msl -msty=application/vnd.muvee.style -mts=model/vnd.mts -mus=application/vnd.musician -musicxml=application/vnd.recordare.musicxml+xml -mvb=application/x-msmediaview -mwf=application/vnd.mfer -mxf=application/mxf -mxl=application/vnd.recordare.musicxml -mxml=application/xv+xml -mxs=application/vnd.triscape.mxs -mxu=video/vnd.mpegurl -n-gage=application/vnd.nokia.n-gage.symbian.install -nb=application/mathematica -nc=application/x-netcdf -ncx=application/x-dtbncx+xml -ngdat=application/vnd.nokia.n-gage.data -nlu=application/vnd.neurolanguage.nlu -nml=application/vnd.enliven -nnd=application/vnd.noblenet-directory -nns=application/vnd.noblenet-sealer -nnw=application/vnd.noblenet-web -npx=image/vnd.net-fpx -nsf=application/vnd.lotus-notes -nws=message/rfc822 -o=application/octet-stream -oa2=application/vnd.fujitsu.oasys2 -oa3=application/vnd.fujitsu.oasys3 -oas=application/vnd.fujitsu.oasys -obd=application/x-msbinder -obj=application/octet-stream -oda=application/oda -odb=application/vnd.oasis.opendocument.database -odc=application/vnd.oasis.opendocument.chart -odf=application/vnd.oasis.opendocument.formula -odft=application/vnd.oasis.opendocument.formula-template -odg=application/vnd.oasis.opendocument.graphics -odi=application/vnd.oasis.opendocument.image -odp=application/vnd.oasis.opendocument.presentation -ods=application/vnd.oasis.opendocument.spreadsheet -odt=application/vnd.oasis.opendocument.text -oga=audio/ogg -ogg=audio/ogg -ogv=video/ogg -ogx=application/ogg -onepkg=application/onenote -onetmp=application/onenote -onetoc=application/onenote -onetoc2=application/onenote -opf=application/oebps-package+xml -oprc=application/vnd.palm -org=application/vnd.lotus-organizer -osf=application/vnd.yamaha.openscoreformat -osfpvg=application/vnd.yamaha.openscoreformat.osfpvg+xml -otc=application/vnd.oasis.opendocument.chart-template -otf=application/x-font-otf -otg=application/vnd.oasis.opendocument.graphics-template -oth=application/vnd.oasis.opendocument.text-web -oti=application/vnd.oasis.opendocument.image-template -otm=application/vnd.oasis.opendocument.text-master -otp=application/vnd.oasis.opendocument.presentation-template -ots=application/vnd.oasis.opendocument.spreadsheet-template -ott=application/vnd.oasis.opendocument.text-template -oxt=application/vnd.openofficeorg.extension -p=text/x-pascal -p10=application/pkcs10 -p12=application/x-pkcs12 -p7b=application/x-pkcs7-certificates -p7c=application/pkcs7-mime -p7m=application/pkcs7-mime -p7r=application/x-pkcs7-certreqresp -p7s=application/pkcs7-signature -pas=text/x-pascal -pbd=application/vnd.powerbuilder6 -pbm=image/x-portable-bitmap -pcf=application/x-font-pcf -pcl=application/vnd.hp-pcl -pclxl=application/vnd.hp-pclxl -pct=image/x-pict -pcurl=application/vnd.curl.pcurl -pcx=image/x-pcx -pdb=application/vnd.palm -pdf=application/pdf -pfa=application/x-font-type1 -pfb=application/x-font-type1 -pfm=application/x-font-type1 -pfr=application/font-tdpfr -pfx=application/x-pkcs12 -pgm=image/x-portable-graymap -pgn=application/x-chess-pgn -pgp=application/pgp-encrypted -pic=image/x-pict -pkg=application/octet-stream -pki=application/pkixcmp -pkipath=application/pkix-pkipath -pl=text/plain -plb=application/vnd.3gpp.pic-bw-large -plc=application/vnd.mobius.plc -plf=application/vnd.pocketlearn -pls=application/pls+xml -pml=application/vnd.ctc-posml -png=image/png -pnm=image/x-portable-anymap -portpkg=application/vnd.macports.portpkg -pot=application/vnd.ms-powerpoint -potm=application/vnd.ms-powerpoint.template.macroenabled.12 -potx=application/vnd.openxmlformats-officedocument.presentationml.template -ppa=application/vnd.ms-powerpoint -ppam=application/vnd.ms-powerpoint.addin.macroenabled.12 -ppd=application/vnd.cups-ppd -ppm=image/x-portable-pixmap -pps=application/vnd.ms-powerpoint -ppsm=application/vnd.ms-powerpoint.slideshow.macroenabled.12 -ppsx=application/vnd.openxmlformats-officedocument.presentationml.slideshow -ppt=application/vnd.ms-powerpoint -pptm=application/vnd.ms-powerpoint.presentation.macroenabled.12 -pptx=application/vnd.openxmlformats-officedocument.presentationml.presentation -pqa=application/vnd.palm -prc=application/x-mobipocket-ebook -pre=application/vnd.lotus-freelance -prf=application/pics-rules -ps=application/postscript -psb=application/vnd.3gpp.pic-bw-small -psd=image/vnd.adobe.photoshop -psf=application/x-font-linux-psf -ptid=application/vnd.pvi.ptid1 -pub=application/x-mspublisher -pvb=application/vnd.3gpp.pic-bw-var -pwn=application/vnd.3m.post-it-notes -pwz=application/vnd.ms-powerpoint -py=text/x-python -pya=audio/vnd.ms-playready.media.pya -pyc=application/x-python-code -pyo=application/x-python-code -pyv=video/vnd.ms-playready.media.pyv -qam=application/vnd.epson.quickanime -qbo=application/vnd.intu.qbo -qfx=application/vnd.intu.qfx -qps=application/vnd.publishare-delta-tree -qt=video/quicktime -qwd=application/vnd.quark.quarkxpress -qwt=application/vnd.quark.quarkxpress -qxb=application/vnd.quark.quarkxpress -qxd=application/vnd.quark.quarkxpress -qxl=application/vnd.quark.quarkxpress -qxt=application/vnd.quark.quarkxpress -ra=audio/x-pn-realaudio -ram=audio/x-pn-realaudio -rar=application/x-rar-compressed -ras=image/x-cmu-raster -rcprofile=application/vnd.ipunplugged.rcprofile -rdf=application/rdf+xml -rdz=application/vnd.data-vision.rdz -rep=application/vnd.businessobjects -res=application/x-dtbresource+xml -rgb=image/x-rgb -rif=application/reginfo+xml -rl=application/resource-lists+xml -rlc=image/vnd.fujixerox.edmics-rlc -rld=application/resource-lists-diff+xml -rm=application/vnd.rn-realmedia -rmi=audio/midi -rmp=audio/x-pn-realaudio-plugin -rms=application/vnd.jcp.javame.midlet-rms -rnc=application/relax-ng-compact-syntax -roff=text/troff -rpm=application/x-rpm -rpss=application/vnd.nokia.radio-presets -rpst=application/vnd.nokia.radio-preset -rq=application/sparql-query -rs=application/rls-services+xml -rsd=application/rsd+xml -rss=application/rss+xml -rtf=application/rtf -rtx=text/richtext -s=text/x-asm -saf=application/vnd.yamaha.smaf-audio -sbml=application/sbml+xml -sc=application/vnd.ibm.secure-container -scd=application/x-msschedule -scm=application/vnd.lotus-screencam -scq=application/scvp-cv-request -scs=application/scvp-cv-response -scurl=text/vnd.curl.scurl -sda=application/vnd.stardivision.draw -sdc=application/vnd.stardivision.calc -sdd=application/vnd.stardivision.impress -sdkd=application/vnd.solent.sdkm+xml -sdkm=application/vnd.solent.sdkm+xml -sdp=application/sdp -sdw=application/vnd.stardivision.writer -see=application/vnd.seemail -seed=application/vnd.fdsn.seed -sema=application/vnd.sema -semd=application/vnd.semd -semf=application/vnd.semf -ser=application/java-serialized-object -setpay=application/set-payment-initiation -setreg=application/set-registration-initiation -sfd-hdstx=application/vnd.hydrostatix.sof-data -sfs=application/vnd.spotfire.sfs -sgl=application/vnd.stardivision.writer-global -sgm=text/sgml -sgml=text/sgml -sh=application/x-sh -shar=application/x-shar -shf=application/shf+xml -si=text/vnd.wap.si -sic=application/vnd.wap.sic -sig=application/pgp-signature -silo=model/mesh -sis=application/vnd.symbian.install -sisx=application/vnd.symbian.install -sit=application/x-stuffit -sitx=application/x-stuffitx -skd=application/vnd.koan -skm=application/vnd.koan -skp=application/vnd.koan -skt=application/vnd.koan -sl=text/vnd.wap.sl -slc=application/vnd.wap.slc -sldm=application/vnd.ms-powerpoint.slide.macroenabled.12 -sldx=application/vnd.openxmlformats-officedocument.presentationml.slide -slt=application/vnd.epson.salt -smf=application/vnd.stardivision.math -smi=application/smil+xml -smil=application/smil+xml -snd=audio/basic -snf=application/x-font-snf -so=application/octet-stream -spc=application/x-pkcs7-certificates -spf=application/vnd.yamaha.smaf-phrase -spl=application/x-futuresplash -spot=text/vnd.in3d.spot -spp=application/scvp-vp-response -spq=application/scvp-vp-request -spx=audio/ogg -src=application/x-wais-source -srx=application/sparql-results+xml -sse=application/vnd.kodak-descriptor -ssf=application/vnd.epson.ssf -ssml=application/ssml+xml -stc=application/vnd.sun.xml.calc.template -std=application/vnd.sun.xml.draw.template -stf=application/vnd.wt.stf -sti=application/vnd.sun.xml.impress.template -stk=application/hyperstudio -stl=application/vnd.ms-pki.stl -str=application/vnd.pg.format -stw=application/vnd.sun.xml.writer.template -sus=application/vnd.sus-calendar -susp=application/vnd.sus-calendar -sv4cpio=application/x-sv4cpio -sv4crc=application/x-sv4crc -svd=application/vnd.svd -svg=image/svg+xml -svgz=image/svg+xml -swa=application/x-director -swf=application/x-shockwave-flash -swi=application/vnd.arastra.swi -sxc=application/vnd.sun.xml.calc -sxd=application/vnd.sun.xml.draw -sxg=application/vnd.sun.xml.writer.global -sxi=application/vnd.sun.xml.impress -sxm=application/vnd.sun.xml.math -sxw=application/vnd.sun.xml.writer -t=text/troff -tao=application/vnd.tao.intent-module-archive -tar=application/x-tar -tcap=application/vnd.3gpp2.tcap -tcl=application/x-tcl -teacher=application/vnd.smart.teacher -tex=application/x-tex -texi=application/x-texinfo -texinfo=application/x-texinfo -text=text/plain -tfm=application/x-tex-tfm -tgz=application/x-gzip -tif=image/tiff -tiff=image/tiff -tmo=application/vnd.tmobile-livetv -torrent=application/x-bittorrent -tpl=application/vnd.groove-tool-template -tpt=application/vnd.trid.tpt -tr=text/troff -tra=application/vnd.trueapp -trm=application/x-msterminal -tsv=text/tab-separated-values -ttc=application/x-font-ttf -ttf=application/x-font-ttf -twd=application/vnd.simtech-mindmapper -twds=application/vnd.simtech-mindmapper -txd=application/vnd.genomatix.tuxedo -txf=application/vnd.mobius.txf -txt=text/plain -u32=application/x-authorware-bin -udeb=application/x-debian-package -ufd=application/vnd.ufdl -ufdl=application/vnd.ufdl -umj=application/vnd.umajin -unityweb=application/vnd.unity -uoml=application/vnd.uoml+xml -uri=text/uri-list -uris=text/uri-list -urls=text/uri-list -ustar=application/x-ustar -utz=application/vnd.uiq.theme -uu=text/x-uuencode -vcd=application/x-cdlink -vcf=text/x-vcard -vcg=application/vnd.groove-vcard -vcs=text/x-vcalendar -vcx=application/vnd.vcx -vis=application/vnd.visionary -viv=video/vnd.vivo -vor=application/vnd.stardivision.writer -vox=application/x-authorware-bin -vrml=model/vrml -vsd=application/vnd.visio -vsf=application/vnd.vsf -vss=application/vnd.visio -vst=application/vnd.visio -vsw=application/vnd.visio -vtu=model/vnd.vtu -vxml=application/voicexml+xml -w3d=application/x-director -wad=application/x-doom -wav=audio/x-wav -wax=audio/x-ms-wax -wbmp=image/vnd.wap.wbmp -wbs=application/vnd.criticaltools.wbs+xml -wbxml=application/vnd.wap.wbxml -wcm=application/vnd.ms-works -wdb=application/vnd.ms-works -wiz=application/msword -wks=application/vnd.ms-works -wm=video/x-ms-wm -wma=audio/x-ms-wma -wmd=application/x-ms-wmd -wmf=application/x-msmetafile -wml=text/vnd.wap.wml -wmlc=application/vnd.wap.wmlc -wmls=text/vnd.wap.wmlscript -wmlsc=application/vnd.wap.wmlscriptc -wmv=video/x-ms-wmv -wmx=video/x-ms-wmx -wmz=application/x-ms-wmz -wpd=application/vnd.wordperfect -wpl=application/vnd.ms-wpl -wps=application/vnd.ms-works -wqd=application/vnd.wqd -wri=application/x-mswrite -wrl=model/vrml -wsdl=application/wsdl+xml -wspolicy=application/wspolicy+xml -wtb=application/vnd.webturbo -wvx=video/x-ms-wvx -x32=application/x-authorware-bin -x3d=application/vnd.hzn-3d-crossword -xap=application/x-silverlight-app -xar=application/vnd.xara -xbap=application/x-ms-xbap -xbd=application/vnd.fujixerox.docuworks.binder -xbm=image/x-xbitmap -xdm=application/vnd.syncml.dm+xml -xdp=application/vnd.adobe.xdp+xml -xdw=application/vnd.fujixerox.docuworks -xenc=application/xenc+xml -xer=application/patch-ops-error+xml -xfdf=application/vnd.adobe.xfdf -xfdl=application/vnd.xfdl -xht=application/xhtml+xml -xhtml=application/xhtml+xml -xhvml=application/xv+xml -xif=image/vnd.xiff -xla=application/vnd.ms-excel -xlam=application/vnd.ms-excel.addin.macroenabled.12 -xlb=application/vnd.ms-excel -xlc=application/vnd.ms-excel -xlm=application/vnd.ms-excel -xls=application/vnd.ms-excel -xlsb=application/vnd.ms-excel.sheet.binary.macroenabled.12 -xlsm=application/vnd.ms-excel.sheet.macroenabled.12 -xlsx=application/vnd.openxmlformats-officedocument.spreadsheetml.sheet -xlt=application/vnd.ms-excel -xltm=application/vnd.ms-excel.template.macroenabled.12 -xltx=application/vnd.openxmlformats-officedocument.spreadsheetml.template -xlw=application/vnd.ms-excel -xml=application/xml -xo=application/vnd.olpc-sugar -xop=application/xop+xml -xpdl=application/xml -xpi=application/x-xpinstall -xpm=image/x-xpixmap -xpr=application/vnd.is-xpr -xps=application/vnd.ms-xpsdocument -xpw=application/vnd.intercon.formnet -xpx=application/vnd.intercon.formnet -xsl=application/xml -xslt=application/xslt+xml -xsm=application/vnd.syncml+xml -xspf=application/xspf+xml -xul=application/vnd.mozilla.xul+xml -xvm=application/xv+xml -xvml=application/xv+xml -xwd=image/x-xwindowdump -xyz=chemical/x-xyz -zaz=application/vnd.zzazz.deck+xml -zip=application/zip -zir=application/vnd.zul -zirz=application/vnd.zul -zmm=application/vnd.handheld-entertainment+xml \ No newline at end of file diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyHttpdTest.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyHttpdTest.java index b550a49..4944ba8 100755 --- a/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyHttpdTest.java +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/TeenyHttpdTest.java @@ -1,8 +1,9 @@ package net.jonathangiles.tools.teenyhttpd; -import net.jonathangiles.tools.teenyhttpd.request.Method; -import net.jonathangiles.tools.teenyhttpd.response.StatusCode; -import net.jonathangiles.tools.teenyhttpd.response.StringResponse; +import net.jonathangiles.tools.teenyhttpd.model.Header; +import net.jonathangiles.tools.teenyhttpd.model.Method; +import net.jonathangiles.tools.teenyhttpd.model.Response; +import net.jonathangiles.tools.teenyhttpd.model.StatusCode; import org.apache.http.HttpResponse; import org.apache.http.client.methods.*; import org.apache.http.entity.StringEntity; @@ -14,13 +15,14 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.List; import java.util.concurrent.CompletableFuture; import static org.junit.jupiter.api.Assertions.*; public class TeenyHttpdTest { - private static final int TEST_PORT = 8080; + private static final int TEST_PORT = 8081; private TeenyHttpd server; @@ -98,7 +100,7 @@ public void testGetMultipleStaticStringRouteRequest() throws Exception { public void testGetRequestWithPathParam() throws Exception { server.addGetRoute("/user/:id", request -> { String id = request.getPathParams().get("id"); - return new StringResponse(StatusCode.OK, "User ID: " + id); + return Response.create(StatusCode.OK, "User ID: " + id); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/user/123"); @@ -112,7 +114,7 @@ public void testGetRequestWithMultiplePathParams() throws Exception { server.addGetRoute("/user/:id/:name", request -> { String id = request.getPathParams().get("id"); String name = request.getPathParams().get("name"); - return new StringResponse(StatusCode.OK, "User ID: " + id + ", Name: " + name); + return Response.create(StatusCode.OK, "User ID: " + id + ", Name: " + name); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/user/123/john"); @@ -125,7 +127,7 @@ public void testGetRequestWithMultiplePathParams() throws Exception { public void testGetRequestWithQueryParam() throws Exception { server.addGetRoute("/search", request -> { String query = request.getQueryParams().get("query"); - return new StringResponse(StatusCode.OK, "Search Query: " + query); + return Response.create(StatusCode.OK, "Search Query: " + query); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/search?query=test"); @@ -139,7 +141,7 @@ public void testGetRequestWithMultipleQueryParams() throws Exception { server.addGetRoute("/search", request -> { String query = request.getQueryParams().get("query"); String sort = request.getQueryParams().get("sort"); - return new StringResponse(StatusCode.OK, "Search Query: " + query + ", Sort: " + sort); + return Response.create(StatusCode.OK, "Search Query: " + query + ", Sort: " + sort); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/search?query=test&sort=desc"); @@ -152,7 +154,7 @@ public void testGetRequestWithMultipleQueryParams() throws Exception { public void testGetRequestWithNonexistentPathParam() throws Exception { server.addGetRoute("/user/:id", request -> { String id = request.getPathParams().get("id"); - return new StringResponse(StatusCode.OK, "User ID: " + id); + return Response.create(StatusCode.OK, "User ID: " + id); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/user"); @@ -164,7 +166,7 @@ public void testGetRequestWithNonexistentPathParam() throws Exception { public void testGetRequestWithEmptyPathParam() throws Exception { server.addGetRoute("/user/:id", request -> { String id = request.getPathParams().get("id"); - return new StringResponse(StatusCode.OK, "User ID: " + id); + return Response.create(StatusCode.OK, "User ID: " + id); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/user/"); @@ -177,7 +179,7 @@ public void testGetRequestWithEmptyPathParam() throws Exception { public void testGetRequestWithNonexistentQueryParam() throws Exception { server.addGetRoute("/search", request -> { String query = request.getQueryParams().get("query"); - return new StringResponse(StatusCode.OK, "Search Query: " + query); + return Response.create(StatusCode.OK, "Search Query: " + query); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/search"); @@ -190,7 +192,7 @@ public void testGetRequestWithNonexistentQueryParam() throws Exception { public void testGetRequestWithEmptyQueryParam() throws Exception { server.addGetRoute("/search", request -> { String query = request.getQueryParams().get("query"); - return new StringResponse(StatusCode.OK, "Search Query: " + query); + return Response.create(StatusCode.OK, "Search Query: " + query); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/search?query="); @@ -203,7 +205,7 @@ public void testGetRequestWithEmptyQueryParam() throws Exception { public void testGetRequestWithSpecialCharactersInPathParam() throws Exception { server.addGetRoute("/user/:id", request -> { String id = request.getPathParams().get("id"); - return new StringResponse(StatusCode.OK, "User ID: " + id); + return Response.create(StatusCode.OK, "User ID: " + id); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/user/john%20doe"); @@ -216,7 +218,7 @@ public void testGetRequestWithSpecialCharactersInPathParam() throws Exception { public void testGetRequestWithPathParamUrlEncoded() throws Exception { server.addGetRoute("/user/:id", request -> { String id = request.getPathParams().get("id"); - return new StringResponse(StatusCode.OK, "User ID: " + id); + return Response.create(StatusCode.OK, "User ID: " + id); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/user/john%2Fdoe"); @@ -229,7 +231,7 @@ public void testGetRequestWithPathParamUrlEncoded() throws Exception { public void testGetRequestWithSpecialCharactersInQueryParam() throws Exception { server.addGetRoute("/search", request -> { String query = request.getQueryParams().get("query"); - return new StringResponse(StatusCode.OK, "Search Query: " + query); + return Response.create(StatusCode.OK, "Search Query: " + query); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/search?query=java%20script"); @@ -242,7 +244,7 @@ public void testGetRequestWithSpecialCharactersInQueryParam() throws Exception { public void testGetRequestWithQueryParamUrlEncoded() throws Exception { server.addGetRoute("/search", request -> { String query = request.getQueryParams().get("query"); - return new StringResponse(StatusCode.OK, "Search Query: " + query); + return Response.create(StatusCode.OK, "Search Query: " + query); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/search?query=java%2Fscript"); @@ -255,7 +257,7 @@ public void testGetRequestWithQueryParamUrlEncoded() throws Exception { public void testGetRequestWithNonAlphanumericPathParam() throws Exception { server.addGetRoute("/user/:id", request -> { String id = request.getPathParams().get("id"); - return new StringResponse(StatusCode.OK, "User ID: " + id); + return Response.create(StatusCode.OK, "User ID: " + id); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/user/()()()"); @@ -268,7 +270,7 @@ public void testGetRequestWithNonAlphanumericPathParam() throws Exception { public void testGetRequestWithNonAlphanumericQueryParam() throws Exception { server.addGetRoute("/search", request -> { String query = request.getQueryParams().get("query"); - return new StringResponse(StatusCode.OK, "Search Query: " + query); + return Response.create(StatusCode.OK, "Search Query: " + query); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/search?query=()()()"); @@ -279,7 +281,7 @@ public void testGetRequestWithNonAlphanumericQueryParam() throws Exception { @Test public void testPostRequest() throws Exception { - server.addRoute(Method.POST, "/post", request -> new StringResponse(StatusCode.OK, "Post request received")); + server.addRoute(Method.POST, "/post", request -> Response.create(StatusCode.OK, "Post request received")); HttpPost postRequest = new HttpPost("http://localhost:" + TEST_PORT + "/post"); postRequest.setEntity(new StringEntity("test")); @@ -291,7 +293,7 @@ public void testPostRequest() throws Exception { @Test public void testPutRequest() throws Exception { - server.addRoute(Method.PUT,"/put", request -> new StringResponse(StatusCode.OK, "Put request received")); + server.addRoute(Method.PUT,"/put", request -> Response.create(StatusCode.OK, "Put request received")); HttpPut putRequest = new HttpPut("http://localhost:" + TEST_PORT + "/put"); putRequest.setEntity(new StringEntity("test")); @@ -303,7 +305,7 @@ public void testPutRequest() throws Exception { @Test public void testDeleteRequest() throws Exception { - server.addRoute(Method.DELETE, "/delete", request -> new StringResponse(StatusCode.OK, "Delete request received")); + server.addRoute(Method.DELETE, "/delete", request -> Response.create(StatusCode.OK, "Delete request received")); HttpDelete deleteRequest = new HttpDelete("http://localhost:" + TEST_PORT + "/delete"); HttpResponse response = httpClient.execute(deleteRequest); @@ -314,7 +316,7 @@ public void testDeleteRequest() throws Exception { @Test public void testHeadRequest() throws Exception { - server.addRoute(Method.HEAD, "/head", request -> new StringResponse(StatusCode.OK, "Head request received")); + server.addRoute(Method.HEAD, "/head", request -> Response.create(StatusCode.OK, "Head request received")); HttpHead headRequest = new HttpHead("http://localhost:" + TEST_PORT + "/head"); HttpResponse response = httpClient.execute(headRequest); @@ -328,7 +330,7 @@ public void testPostRequestWithPathAndQueryParams() throws Exception { server.addRoute(Method.POST, "/post/:id", request -> { String id = request.getPathParams().get("id"); String content = request.getQueryParams().get("content"); - return new StringResponse(StatusCode.OK, "Post request received with ID: " + id + " and content: " + content); + return Response.create(StatusCode.OK, "Post request received with ID: " + id + " and content: " + content); }); HttpPost postRequest = new HttpPost("http://localhost:" + TEST_PORT + "/post/123?content=test"); @@ -344,7 +346,7 @@ public void testPutRequestWithPathAndQueryParams() throws Exception { server.addRoute(Method.PUT, "/put/:id", request -> { String id = request.getPathParams().get("id"); String content = request.getQueryParams().get("content"); - return new StringResponse(StatusCode.OK, "Put request received with ID: " + id + " and content: " + content); + return Response.create(StatusCode.OK, "Put request received with ID: " + id + " and content: " + content); }); HttpPut putRequest = new HttpPut("http://localhost:" + TEST_PORT + "/put/123?content=test"); @@ -360,7 +362,7 @@ public void testDeleteRequestWithPathAndQueryParams() throws Exception { server.addRoute(Method.DELETE, "/delete/:id", request -> { String id = request.getPathParams().get("id"); String content = request.getQueryParams().get("content"); - return new StringResponse(StatusCode.OK, "Delete request received with ID: " + id + " and content: " + content); + return Response.create(StatusCode.OK, "Delete request received with ID: " + id + " and content: " + content); }); HttpDelete deleteRequest = new HttpDelete("http://localhost:" + TEST_PORT + "/delete/123?content=test"); @@ -375,7 +377,7 @@ public void testHeadRequestWithPathAndQueryParams() throws Exception { server.addRoute(Method.HEAD, "/head/:id", request -> { String id = request.getPathParams().get("id"); String content = request.getQueryParams().get("content"); - return new StringResponse(StatusCode.OK, "Head request received with ID: " + id + " and content: " + content); + return Response.create(StatusCode.OK, "Head request received with ID: " + id + " and content: " + content); }); HttpHead headRequest = new HttpHead("http://localhost:" + TEST_PORT + "/head/123?content=test"); @@ -435,15 +437,70 @@ public void testUnsupportedMethodReturns405() throws Exception { public void testMultipleGetMethods() throws Exception { server.addGetRoute("/user/:id/details", request -> { String id = request.getPathParams().get("id"); - return new StringResponse(StatusCode.OK, "User ID: " + id); + return Response.create(StatusCode.OK, "User ID: " + id); }); server.addGetRoute("/QueryParams", request -> { request.getQueryParams().forEach((key, value) -> System.out.println(key + " = " + value)); - return new StringResponse(StatusCode.OK, "Query Params: " + request.getQueryParams()); + return Response.create(StatusCode.OK, "Query Params: " + request.getQueryParams()); }); HttpResponse response = executeRequest(Method.GET, "http://localhost:" + TEST_PORT + "/QueryParams?test=123&test2=456"); assertEquals(200, response.getStatusLine().getStatusCode()); assertEquals("Query Params: {test2=456, test=123}", EntityUtils.toString(response.getEntity())); } + + @Test + public void testSingleValueHeader() { + Header header = new Header("Content-Type: text/html"); + assertEquals("Content-Type", header.getKey()); + List values = header.getValues(); + assertEquals(1, values.size()); + assertEquals("text/html", values.get(0)); + } + + @Test + public void testMultiValueHeader() { + Header header = new Header("Accept: text/html, application/xhtml+xml, application/xml"); + assertEquals("Accept", header.getKey()); + List values = header.getValues(); + assertEquals(3, values.size()); + assertEquals("text/html", values.get(0)); + assertEquals("application/xhtml+xml", values.get(1)); + assertEquals("application/xml", values.get(2)); + } + + + // ---------------------------- + // Server-Sent Events + // ---------------------------- +// @Test +// public void testSse() throws IOException { +// ServerSentEvent sse = new ServerSentEvent() { +// @Override +// public void onActive() { +// sendMessage("Test Message 1"); +// sendMessage("Test Message 2"); +// sendMessage("Test Message 3"); +//// close(); +// } +// +// @Override +// public void onInactive() { +// +// } +// }; +// server.addServerSentEventRoute("/events", sse); +// +// List messages = new ArrayList<>(); +// HttpGet request = new HttpGet("http://localhost:" + TEST_PORT + "/events"); +// HttpResponse response = httpClient.execute(request); +// BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); +// String line; +// while ((line = reader.readLine()) != null) { +// if (line.startsWith("data:")) { +// messages.add(line.substring(5).trim()); +// } +// } +// assertEquals(Arrays.asList("Test Message 1", "Test Message 2", "Test Message 3"), messages); +// } } \ No newline at end of file diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/TestServer.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/TestServer.java index 351b6a1..d62995a 100755 --- a/src/test/java/net/jonathangiles/tools/teenyhttpd/TestServer.java +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/TestServer.java @@ -1,9 +1,11 @@ package net.jonathangiles.tools.teenyhttpd; -import net.jonathangiles.tools.teenyhttpd.response.StatusCode; -import net.jonathangiles.tools.teenyhttpd.response.StringResponse; +import net.jonathangiles.tools.teenyhttpd.model.ServerSentEventHandler; +import net.jonathangiles.tools.teenyhttpd.model.ServerSentEventMessage; +import net.jonathangiles.tools.teenyhttpd.model.Response; +import net.jonathangiles.tools.teenyhttpd.model.StatusCode; -import java.io.File; +import java.util.concurrent.atomic.AtomicInteger; public class TestServer { @@ -17,7 +19,7 @@ public static void main(String[] args) { server.addGetRoute("/user/:id/details", request -> { String id = request.getPathParams().get("id"); - return new StringResponse(StatusCode.OK, "User ID: " + id); + return Response.create(StatusCode.OK, "User ID: " + id); }); // // server.addGetRoute("/foo/:bar/:baz", request -> { @@ -31,8 +33,51 @@ public static void main(String[] args) { return StatusCode.OK.asResponse(); }); + final ServerSentEventHandler sse = ServerSentEventHandler.create((ServerSentEventHandler _sse) -> { + System.out.println("SSE active - sending messages to client(s)"); + + // start a thread and send messages to the client(s) + new Thread(() -> { + // all clients share the same integer value, but they get a custom message based + // on the path parameter for :username + AtomicInteger i = new AtomicInteger(0); + + while (_sse.hasActiveConnections()) { + _sse.sendMessage(client -> { + String username = client.getPathParams().get("username"); + return new ServerSentEventMessage("Hello " + username + " - " + i, "counter"); + }); + i.incrementAndGet(); +// sendMessage(new ServerSentEventMessage("Message " + i++, "counter")); + System.out.println(i); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }).start(); + }); + server.addServerSentEventRoute("/sse/:username", sse); + + server.addServerSentEventRoute("/events", ServerSentEventHandler.create(simpleSse -> new Thread(() -> { + int i = 0; + while (simpleSse.hasActiveConnections()) { + simpleSse.sendMessage(new ServerSentEventMessage("Message " + i++, "counter")); + threadSleep(1000); + } + }).start())); + // server.addFileRoute("/"); server.start(); } + + private static void threadSleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } } diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/chat/ChatServer.java b/src/test/java/net/jonathangiles/tools/teenyhttpd/chat/ChatServer.java new file mode 100755 index 0000000..f65c2c2 --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/chat/ChatServer.java @@ -0,0 +1,87 @@ +package net.jonathangiles.tools.teenyhttpd.chat; + +import net.jonathangiles.tools.teenyhttpd.TeenyHttpd; +import net.jonathangiles.tools.teenyhttpd.model.*; + +import java.io.File; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Start this app then browse to http://localhost to see the chat app. + */ +public class ChatServer { + // collection of all connected users + private final Set users = Collections.synchronizedSet(new HashSet<>()); + + // event handler for sending chat messages to all clients + private final ServerSentEventHandler chatMessagesEventHandler = ServerSentEventHandler.create(); + + // event handler for sending all connected users to all clients + private final ServerSentEventHandler usersEventHandler = ServerSentEventHandler.create(); + + public static void main(String[] args) { + new ChatServer().start(); + } + + private void start() { + final int PORT = 80; + + TeenyHttpd server = new TeenyHttpd(PORT); + + // post a JSON message to this endpoint to send a message to all clients. + // JSON format is {"user":"", "message":""} + server.addRoute(Method.POST, "/message", request -> { + String json = request.getQueryParams().get("message"); + if (json != null && !json.isEmpty()) { + sendMessage(json); + } + return StatusCode.OK.asResponse(); + }); + + // Post a username to this endpoint to join the chat and broadcast you are connected to all other users + server.addRoute(Method.POST, "/login", request -> { + String username = request.getQueryParams().get("username"); + if (username != null && !username.isEmpty()) { + users.add(username); + usersEventHandler.sendMessage(String.join(",", users)); + sendSystemMessage(username + " has joined the chat."); // Send system message + } + return StatusCode.OK.asResponse(); + }); + + // post a username to this endpoint to leave the chat and broadcast you are disconnected to all other users + server.addRoute(Method.POST, "/logout", request -> { + String username = request.getQueryParams().get("username"); + if (username != null && !username.isEmpty()) { + users.remove(username); + usersEventHandler.sendMessage(String.join(",", users)); + sendSystemMessage(username + " has left the chat."); // Send system message + } + return StatusCode.OK.asResponse(); + }); + + // The two SSE endpoints, for messages and connected users + server.addServerSentEventRoute("/messages", chatMessagesEventHandler); + server.addServerSentEventRoute("/users", usersEventHandler); + + // we serve the web page from here + server.addFileRoute("/", new File("src/test/java/net/jonathangiles/tools/teenyhttpd/chat")); + + server.start(); + } + + private void sendSystemMessage(String message) { + sendMessage("System", message); + } + + private void sendMessage(String user, String message) { + String jsonString = String.format("{\"user\":\"%s\", \"message\":\"%s\"}", user, message); + sendMessage(jsonString); + } + + private void sendMessage(String json) { + chatMessagesEventHandler.sendMessage(json); + } +} diff --git a/src/test/java/net/jonathangiles/tools/teenyhttpd/chat/index.html b/src/test/java/net/jonathangiles/tools/teenyhttpd/chat/index.html new file mode 100644 index 0000000..b33243d --- /dev/null +++ b/src/test/java/net/jonathangiles/tools/teenyhttpd/chat/index.html @@ -0,0 +1,288 @@ + + + + Chat + + + + + + + + + +
+
+
+
+
Online Users
+ +
+
+
+
+
+ + + +
+
+
+
+ + + + + \ No newline at end of file