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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file