diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..dc9ac7502
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+
+.idea
+*.iml
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 000000000..9bcf99945
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,3 @@
+language: java
+jdk:
+ - oraclejdk8
diff --git a/README.md b/README.md
new file mode 100644
index 000000000..7cb1c15da
--- /dev/null
+++ b/README.md
@@ -0,0 +1,2 @@
+# javalin
+Java/Kotlin micro web framework
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 000000000..9d935513c
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,176 @@
+
+ 4.0.0
+
+
+ org.sonatype.oss
+ oss-parent
+ 7
+
+
+ io.javalin
+ javalin
+ 0.0.1-SNAPSHOT
+
+ Javalin
+ A Java/Kotlin micro web framework
+ https://javalin.io
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+ scm:git:git@github.com:tipsy/javalin.git
+ scm:git:git@github.com:tipsy/javalin.git
+ https://github.com/tipsy/javalin.git
+ HEAD
+
+
+
+ David Åse
+
+
+
+ GitHub Issue Tracker
+ https://github.com/tipsy/javalin/issues
+
+
+
+ 1.8
+ 9.4.5.v20170502
+ UTF-8
+
+
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.13
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.13
+ true
+
+
+
+
+ org.eclipse.jetty
+ jetty-server
+ ${jetty.version}
+
+
+ org.eclipse.jetty
+ jetty-webapp
+ ${jetty.version}
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.6.3
+ true
+
+
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+ org.hamcrest
+ hamcrest-library
+ 1.3
+ test
+
+
+ com.mashape.unirest
+ unirest-java
+ 1.4.9
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.3
+
+ ${java.version}
+ ${java.version}
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-enforcer-plugin
+ 1.4
+
+
+ enforce-java
+
+ enforce
+
+
+
+
+ [${java.version},)
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 2.10.3
+
+
+
+
+
+
+
+
+ sign-artifacts
+
+
+ sign
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.6
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/javalin/ApiBuilder.java b/src/main/java/javalin/ApiBuilder.java
new file mode 100644
index 000000000..c1d9665e8
--- /dev/null
+++ b/src/main/java/javalin/ApiBuilder.java
@@ -0,0 +1,135 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin;
+
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javalin.security.Role;
+
+public class ApiBuilder {
+
+ @FunctionalInterface
+ public interface EndpointGroup {
+ void addEndpoints();
+ }
+
+ static void setStaticJavalin(Javalin javalin) {
+ staticJavalin = javalin;
+ }
+
+ static void clearStaticJavalin() {
+ staticJavalin = null;
+ }
+
+ private static Javalin staticJavalin;
+ private static Deque pathDeque = new ArrayDeque<>();
+
+ public static void path(String path, EndpointGroup endpointGroup) {
+ pathDeque.addLast(path);
+ endpointGroup.addEndpoints();
+ pathDeque.removeLast();
+ }
+
+ private static String prefixPath(String path) {
+ return pathDeque.stream().collect(Collectors.joining("")) + path;
+ }
+
+ // Everything below here is copied from the end of Javalin.java
+
+ // HTTP verbs
+ public static void get(String path, Handler handler) {
+ staticJavalin.get(prefixPath(path), handler);
+ }
+
+ public static void post(String path, Handler handler) {
+ staticJavalin.post(prefixPath(path), handler);
+ }
+
+ public static void put(String path, Handler handler) {
+ staticJavalin.put(prefixPath(path), handler);
+ }
+
+ public static void patch(String path, Handler handler) {
+ staticJavalin.patch(prefixPath(path), handler);
+ }
+
+ public static void delete(String path, Handler handler) {
+ staticJavalin.delete(prefixPath(path), handler);
+ }
+
+ public static void head(String path, Handler handler) {
+ staticJavalin.head(prefixPath(path), handler);
+ }
+
+ public static void trace(String path, Handler handler) {
+ staticJavalin.trace(prefixPath(path), handler);
+ }
+
+ public static void connect(String path, Handler handler) {
+ staticJavalin.connect(prefixPath(path), handler);
+ }
+
+ public static void options(String path, Handler handler) {
+ staticJavalin.options(prefixPath(path), handler);
+ }
+
+ // Secured HTTP verbs
+ public static void get(String path, Handler handler, List permittedRoles) {
+ staticJavalin.get(prefixPath(path), handler, permittedRoles);
+ }
+
+ public static void post(String path, Handler handler, List permittedRoles) {
+ staticJavalin.post(prefixPath(path), handler, permittedRoles);
+ }
+
+ public static void put(String path, Handler handler, List permittedRoles) {
+ staticJavalin.put(prefixPath(path), handler, permittedRoles);
+ }
+
+ public static void patch(String path, Handler handler, List permittedRoles) {
+ staticJavalin.patch(prefixPath(path), handler, permittedRoles);
+ }
+
+ public static void delete(String path, Handler handler, List permittedRoles) {
+ staticJavalin.delete(prefixPath(path), handler, permittedRoles);
+ }
+
+ public static void head(String path, Handler handler, List permittedRoles) {
+ staticJavalin.head(prefixPath(path), handler, permittedRoles);
+ }
+
+ public static void trace(String path, Handler handler, List permittedRoles) {
+ staticJavalin.trace(prefixPath(path), handler, permittedRoles);
+ }
+
+ public static void connect(String path, Handler handler, List permittedRoles) {
+ staticJavalin.connect(prefixPath(path), handler, permittedRoles);
+ }
+
+ public static void options(String path, Handler handler, List permittedRoles) {
+ staticJavalin.options(prefixPath(path), handler, permittedRoles);
+ }
+
+ // Filters
+ public static void before(String path, Handler handler) {
+ staticJavalin.before(prefixPath(path), handler);
+ }
+
+ public static void before(Handler handler) {
+ staticJavalin.before(prefixPath("/*"), handler);
+ }
+
+ public static void after(String path, Handler handler) {
+ staticJavalin.after(prefixPath(path), handler);
+ }
+
+ public static void after(Handler handler) {
+ staticJavalin.after(prefixPath("/*"), handler);
+ }
+
+}
diff --git a/src/main/java/javalin/ErrorHandler.java b/src/main/java/javalin/ErrorHandler.java
new file mode 100644
index 000000000..ae742817e
--- /dev/null
+++ b/src/main/java/javalin/ErrorHandler.java
@@ -0,0 +1,11 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin;
+
+@FunctionalInterface
+public interface ErrorHandler {
+ // very similar to handler, but can't throw exception
+ void handle(Request request, Response response);
+}
diff --git a/src/main/java/javalin/ExceptionHandler.java b/src/main/java/javalin/ExceptionHandler.java
new file mode 100644
index 000000000..ab64c05e9
--- /dev/null
+++ b/src/main/java/javalin/ExceptionHandler.java
@@ -0,0 +1,10 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin;
+
+@FunctionalInterface
+public interface ExceptionHandler {
+ void handle(T exception, Request request, Response response);
+}
diff --git a/src/main/java/javalin/HaltException.java b/src/main/java/javalin/HaltException.java
new file mode 100644
index 000000000..1729c26e9
--- /dev/null
+++ b/src/main/java/javalin/HaltException.java
@@ -0,0 +1,29 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin;
+
+import javax.servlet.http.HttpServletResponse;
+
+public class HaltException extends RuntimeException {
+ public int statusCode = HttpServletResponse.SC_OK;
+ public String body = "Execution halted";
+
+ HaltException() {
+ }
+
+ HaltException(int statusCode) {
+ this.statusCode = statusCode;
+ }
+
+ HaltException(String body) {
+ this.body = body;
+ }
+
+ public HaltException(int statusCode, String body) {
+ this.statusCode = statusCode;
+ this.body = body;
+ }
+
+}
diff --git a/src/main/java/javalin/Handler.java b/src/main/java/javalin/Handler.java
new file mode 100644
index 000000000..4f279f712
--- /dev/null
+++ b/src/main/java/javalin/Handler.java
@@ -0,0 +1,29 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.servlet.http.HttpServletRequest;
+
+@FunctionalInterface
+public interface Handler {
+
+ enum Type {
+ GET, POST, PUT, PATCH, DELETE, HEAD, TRACE, CONNECT, OPTIONS, BEFORE, AFTER, INVALID;
+
+ private static Map methodMap = Stream.of(Type.values()).collect(Collectors.toMap(Object::toString, v -> v));
+
+ public static Type fromServletRequest(HttpServletRequest httpRequest) {
+ String key = Optional.ofNullable(httpRequest.getHeader("X-HTTP-Method-Override")).orElse(httpRequest.getMethod());
+ return methodMap.getOrDefault(key.toUpperCase(), INVALID);
+ }
+ }
+
+ void handle(Request request, Response response) throws Exception;
+}
diff --git a/src/main/java/javalin/Javalin.java b/src/main/java/javalin/Javalin.java
new file mode 100644
index 000000000..44044359b
--- /dev/null
+++ b/src/main/java/javalin/Javalin.java
@@ -0,0 +1,304 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin;
+
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.function.Consumer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javalin.core.ErrorMapper;
+import javalin.core.ExceptionMapper;
+import javalin.core.PathMatcher;
+import javalin.core.util.Util;
+import javalin.embeddedserver.EmbeddedServer;
+import javalin.embeddedserver.EmbeddedServerFactory;
+import javalin.embeddedserver.jetty.EmbeddedJettyFactory;
+import javalin.lifecycle.Event;
+import javalin.lifecycle.EventListener;
+import javalin.lifecycle.EventManager;
+import javalin.security.AccessManager;
+import javalin.security.Role;
+
+public class Javalin {
+
+ private static Logger log = LoggerFactory.getLogger(Javalin.class);
+
+ public static int DEFAULT_PORT = 7000;
+
+ private int port = DEFAULT_PORT;
+
+ private String ipAddress = "0.0.0.0";
+
+ private EmbeddedServer embeddedServer;
+ private EmbeddedServerFactory embeddedServerFactory = new EmbeddedJettyFactory();
+
+ private String staticFileDirectory = null;
+ PathMatcher pathMatcher = new PathMatcher();
+ ExceptionMapper exceptionMapper = new ExceptionMapper();
+ ErrorMapper errorMapper = new ErrorMapper();
+
+ private EventManager eventManager = new EventManager();
+
+ private Consumer startupExceptionHandler = (e) -> log.error("Failed to start Javalin", e);
+
+ private CountDownLatch startLatch = new CountDownLatch(1);
+ private CountDownLatch stopLatch = new CountDownLatch(1);
+
+ private AccessManager accessManager = (Handler handler, Request request, Response response, List permittedRoles) -> {
+ throw new IllegalStateException("No access manager configured. Add an access manager using 'accessManager()'");
+ };
+
+ public Javalin accessManager(AccessManager accessManager) {
+ this.accessManager = accessManager;
+ return this;
+ }
+
+ public static Javalin create() {
+ return new Javalin();
+ }
+
+ private boolean started = false;
+
+ public synchronized Javalin start() {
+ if (!started) {
+ printWelcomeScreen();
+ new Thread(() -> {
+ eventManager.fireEvent(Event.Type.SERVER_STARTING, this);
+ try {
+ embeddedServer = embeddedServerFactory.create(pathMatcher, exceptionMapper, errorMapper, staticFileDirectory);
+ port = embeddedServer.start(ipAddress, port);
+ } catch (Exception e) {
+ startupExceptionHandler.accept(e);
+ }
+ eventManager.fireEvent(Event.Type.SERVER_STARTED, this);
+ try {
+ startLatch.countDown();
+ embeddedServer.join();
+ } catch (InterruptedException e) {
+ log.error("Server startup interrupted", e);
+ Thread.currentThread().interrupt();
+ }
+ }).start();
+ started = true;
+ }
+ return this;
+ }
+
+ public Javalin awaitInitialization() {
+ if (!started) {
+ throw new IllegalStateException("Server hasn't been started. Call start() before calling this method.");
+ }
+ try {
+ startLatch.await();
+ } catch (InterruptedException e) {
+ log.info("awaitInitialization was interrupted");
+ Thread.currentThread().interrupt();
+ }
+ return this;
+ }
+
+ public synchronized Javalin stop() {
+ eventManager.fireEvent(Event.Type.SERVER_STOPPING, this);
+ new Thread(() -> {
+ embeddedServer.stop();
+ started = false;
+ startLatch = new CountDownLatch(1);
+ eventManager.fireEvent(Event.Type.SERVER_STOPPED, this);
+ pathMatcher = new PathMatcher();
+ exceptionMapper = new ExceptionMapper();
+ errorMapper = new ErrorMapper();
+ stopLatch.countDown();
+ stopLatch = new CountDownLatch(1);
+ }).start();
+ return this;
+ }
+
+ public Javalin awaitTermination() {
+ if (!started) {
+ throw new IllegalStateException("Server hasn't been stopped. Call stop() before calling this method.");
+ }
+ try {
+ stopLatch.await();
+ } catch (InterruptedException e) {
+ log.info("awaitTermination was interrupted");
+ Thread.currentThread().interrupt();
+ }
+ return this;
+ }
+
+ public Javalin embeddedServer(EmbeddedServerFactory embeddedServerFactory) {
+ ensureServerHasNotStarted();
+ this.embeddedServerFactory = embeddedServerFactory;
+ return this;
+ }
+
+ /*testing*/ EmbeddedServer embeddedServer() {
+ return embeddedServer;
+ }
+
+ public synchronized Javalin enableStaticFiles(String location) {
+ ensureServerHasNotStarted();
+ Util.notNull("Location cannot be null", location);
+ staticFileDirectory = location;
+ return this;
+ }
+
+ public synchronized Javalin ipAddress(String ipAddress) {
+ ensureServerHasNotStarted();
+ this.ipAddress = ipAddress;
+ return this;
+ }
+
+ public synchronized Javalin port(int port) {
+ ensureServerHasNotStarted();
+ this.port = port;
+ return this;
+ }
+
+ public synchronized Javalin event(Event.Type eventType, EventListener eventListener) {
+ ensureServerHasNotStarted();
+ eventManager.addEventListener(eventType, eventListener);
+ return this;
+ }
+
+ public Javalin startupExceptionHandler(Consumer startupExceptionHandler) {
+ ensureServerHasNotStarted();
+ this.startupExceptionHandler = startupExceptionHandler;
+ return this;
+ }
+
+ private void ensureServerHasNotStarted() {
+ if (started) {
+ throw new IllegalStateException("This must be done before starting the server (adding handlers automatically starts the server)");
+ }
+ }
+
+ public synchronized int port() {
+ return started ? port : -1;
+ }
+
+ public synchronized Javalin exception(Class exceptionClass, ExceptionHandler super T> exceptionHandler) {
+ exceptionMapper.put(exceptionClass, exceptionHandler);
+ return this;
+ }
+
+ public synchronized Javalin error(int statusCode, ErrorHandler errorHandler) {
+ errorMapper.put(statusCode, errorHandler);
+ return this;
+ }
+
+ public Javalin routes(ApiBuilder.EndpointGroup endpointGroup) {
+ ApiBuilder.setStaticJavalin(this);
+ endpointGroup.addEndpoints();
+ ApiBuilder.clearStaticJavalin();
+ return this;
+ }
+
+ public Javalin addHandler(Handler.Type httpMethod, String path, Handler handler) {
+ start();
+ pathMatcher.add(httpMethod, path, handler);
+ return this;
+ }
+
+ // HTTP verbs
+ public Javalin get(String path, Handler handler) {
+ return addHandler(Handler.Type.GET, path, handler);
+ }
+
+ public Javalin post(String path, Handler handler) {
+ return addHandler(Handler.Type.POST, path, handler);
+ }
+
+ public Javalin put(String path, Handler handler) {
+ return addHandler(Handler.Type.PUT, path, handler);
+ }
+
+ public Javalin patch(String path, Handler handler) {
+ return addHandler(Handler.Type.PATCH, path, handler);
+ }
+
+ public Javalin delete(String path, Handler handler) {
+ return addHandler(Handler.Type.DELETE, path, handler);
+ }
+
+ public Javalin head(String path, Handler handler) {
+ return addHandler(Handler.Type.HEAD, path, handler);
+ }
+
+ public Javalin trace(String path, Handler handler) {
+ return addHandler(Handler.Type.TRACE, path, handler);
+ }
+
+ public Javalin connect(String path, Handler handler) {
+ return addHandler(Handler.Type.CONNECT, path, handler);
+ }
+
+ public Javalin options(String path, Handler handler) {
+ return addHandler(Handler.Type.OPTIONS, path, handler);
+ }
+
+ // Secured HTTP verbs
+ public Javalin get(String path, Handler handler, List permittedRoles) {
+ return this.get(path, (req, res) -> accessManager.manage(handler, req, res, permittedRoles));
+ }
+
+ public Javalin post(String path, Handler handler, List permittedRoles) {
+ return this.post(path, (req, res) -> accessManager.manage(handler, req, res, permittedRoles));
+ }
+
+ public Javalin put(String path, Handler handler, List permittedRoles) {
+ return this.put(path, (req, res) -> accessManager.manage(handler, req, res, permittedRoles));
+ }
+
+ public Javalin patch(String path, Handler handler, List permittedRoles) {
+ return this.patch(path, (req, res) -> accessManager.manage(handler, req, res, permittedRoles));
+ }
+
+ public Javalin delete(String path, Handler handler, List permittedRoles) {
+ return this.delete(path, (req, res) -> accessManager.manage(handler, req, res, permittedRoles));
+ }
+
+ public Javalin head(String path, Handler handler, List permittedRoles) {
+ return this.head(path, (req, res) -> accessManager.manage(handler, req, res, permittedRoles));
+ }
+
+ public Javalin trace(String path, Handler handler, List permittedRoles) {
+ return this.trace(path, (req, res) -> accessManager.manage(handler, req, res, permittedRoles));
+ }
+
+ public Javalin connect(String path, Handler handler, List permittedRoles) {
+ return this.connect(path, (req, res) -> accessManager.manage(handler, req, res, permittedRoles));
+ }
+
+ public Javalin options(String path, Handler handler, List permittedRoles) {
+ return this.options(path, (req, res) -> accessManager.manage(handler, req, res, permittedRoles));
+ }
+
+ // Filters
+ public Javalin before(String path, Handler handler) {
+ return addHandler(Handler.Type.BEFORE, path, handler);
+ }
+
+ public Javalin before(Handler handler) {
+ return before("/*", handler);
+ }
+
+ public Javalin after(String path, Handler handler) {
+ return addHandler(Handler.Type.AFTER, path, handler);
+ }
+
+ public Javalin after(Handler handler) {
+ return after("/*", handler);
+ }
+
+ // Very useless stuff
+ private void printWelcomeScreen() {
+ log.info("\n" + Util.javalinBanner());
+ }
+
+}
diff --git a/src/main/java/javalin/Request.java b/src/main/java/javalin/Request.java
new file mode 100644
index 000000000..44c5d4049
--- /dev/null
+++ b/src/main/java/javalin/Request.java
@@ -0,0 +1,188 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+
+import javalin.core.util.RequestUtil;
+
+public class Request {
+
+ private HttpServletRequest servletRequest;
+ private Map paramMap; // cache (different for each handler)
+ private List splatList; // cache (different for each handler)
+
+ public Request(HttpServletRequest httpRequest, Map paramMap, List splatList) {
+ this.servletRequest = httpRequest;
+ this.paramMap = paramMap;
+ this.splatList = splatList;
+ }
+
+ public HttpServletRequest unwrap() {
+ return servletRequest;
+ }
+
+ public String body() {
+ return RequestUtil.byteArrayToString(bodyAsBytes(), servletRequest.getCharacterEncoding());
+ }
+
+ public byte[] bodyAsBytes() {
+ try {
+ return RequestUtil.toByteArray(servletRequest.getInputStream());
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ // yeah, this is probably not the best solution
+ public String bodyParam(String bodyParam) {
+ for (String keyValuePair : body().split("&")) {
+ String[] pair = keyValuePair.split("=");
+ if (pair[0].equalsIgnoreCase(bodyParam)) {
+ return pair[1];
+ }
+ }
+ return null;
+ }
+
+ public String formParam(String formParam) {
+ return bodyParam(formParam);
+ }
+
+ public String param(String param) {
+ if (param == null) {
+ return null;
+ }
+ if (!param.startsWith(":")) {
+ param = ":" + param;
+ }
+ return paramMap.get(param.toLowerCase());
+ }
+
+ public Map paramMap() {
+ return Collections.unmodifiableMap(paramMap);
+ }
+
+ public String splat(int splatNr) {
+ return splatList.get(splatNr);
+ }
+
+ public String[] splats() {
+ return splatList.toArray(new String[splatList.size()]);
+ }
+
+ // wrapper methods for HttpServletRequest
+
+ public void attribute(String attribute, Object value) {
+ servletRequest.setAttribute(attribute, value);
+ }
+
+ public T attribute(String attribute) {
+ return (T) servletRequest.getAttribute(attribute);
+ }
+
+ public Map attributeMap() {
+ return Collections.list(servletRequest.getAttributeNames()).stream().collect(Collectors.toMap(a -> a, this::attribute));
+ }
+
+ public int contentLength() {
+ return servletRequest.getContentLength();
+ }
+
+ public String contentType() {
+ return servletRequest.getContentType();
+ }
+
+ public String cookie(String name) {
+ return Stream.of(Optional.ofNullable(servletRequest.getCookies()).orElse(new Cookie[] {}))
+ .filter(cookie -> cookie.getName().equals(name))
+ .map(Cookie::getValue)
+ .findFirst()
+ .orElse(null);
+ }
+
+ public Map cookieMap() {
+ Cookie[] cookies = Optional.ofNullable(servletRequest.getCookies()).orElse(new Cookie[] {});
+ return Stream.of(cookies).collect(Collectors.toMap(Cookie::getName, Cookie::getValue));
+ }
+
+ public String header(String header) {
+ return servletRequest.getHeader(header);
+ }
+
+ public Map headerMap() {
+ return Collections.list(servletRequest.getHeaderNames()).stream().collect(Collectors.toMap(h -> h, this::header));
+ }
+
+ public String host() {
+ return servletRequest.getHeader("host");
+ }
+
+ public String ip() {
+ return servletRequest.getRemoteAddr();
+ }
+
+ public String path() {
+ return servletRequest.getPathInfo();
+ }
+
+ public int port() {
+ return servletRequest.getServerPort();
+ }
+
+ public String protocol() {
+ return servletRequest.getProtocol();
+ }
+
+ public String queryParam(String queryParam) {
+ return servletRequest.getParameter(queryParam);
+ }
+
+ public String queryParamOrDefault(String queryParam, String defaultValue) {
+ return Optional.ofNullable(servletRequest.getParameter(queryParam)).orElse(defaultValue);
+ }
+
+ public String[] queryParams(String queryParam) {
+ return servletRequest.getParameterValues(queryParam);
+ }
+
+ public Map queryParamMap() {
+ return servletRequest.getParameterMap();
+ }
+
+ public String queryString() {
+ return servletRequest.getQueryString();
+ }
+
+ public String requestMethod() {
+ return servletRequest.getMethod();
+ }
+
+ public String scheme() {
+ return servletRequest.getScheme();
+ }
+
+ public String uri() {
+ return servletRequest.getRequestURI();
+ }
+
+ public String url() {
+ return servletRequest.getRequestURL().toString();
+ }
+
+ public String userAgent() {
+ return servletRequest.getHeader("user-agent");
+ }
+
+}
diff --git a/src/main/java/javalin/Response.java b/src/main/java/javalin/Response.java
new file mode 100644
index 000000000..779790eca
--- /dev/null
+++ b/src/main/java/javalin/Response.java
@@ -0,0 +1,142 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin;
+
+import java.io.IOException;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javalin.builder.CookieBuilder;
+
+import static javalin.builder.CookieBuilder.*;
+
+public class Response {
+
+ private static Logger log = LoggerFactory.getLogger(Response.class);
+
+ private HttpServletResponse servletResponse;
+ private String body;
+ private String encoding;
+
+ public Response(HttpServletResponse servletResponse) {
+ this.servletResponse = servletResponse;
+ }
+
+ public HttpServletResponse unwrap() {
+ return servletResponse;
+ }
+
+ public String contentType() {
+ return servletResponse.getContentType();
+ }
+
+ public Response contentType(String contentType) {
+ servletResponse.setContentType(contentType);
+ return this;
+ }
+
+ public String body() {
+ return body;
+ }
+
+ public Response body(String body) {
+ this.body = body;
+ return this;
+ }
+
+ public String encoding() {
+ return encoding;
+ }
+
+ public Response encoding(String charset) {
+ encoding = charset;
+ return this;
+ }
+
+ public void header(String headerName) {
+ servletResponse.getHeader(headerName);
+ }
+
+ public Response header(String headerName, String headerValue) {
+ servletResponse.setHeader(headerName, headerValue);
+ return this;
+ }
+
+ public Response html(String html) {
+ return body(html).contentType("text/html");
+ }
+
+ public Response json(Object object) {
+ return body(ResponseMapper.toJson(object)).contentType("application/json");
+ }
+
+ public Response redirect(String location) {
+ try {
+ servletResponse.sendRedirect(location);
+ } catch (IOException e) {
+ log.warn("Exception while trying to redirect", e);
+ }
+ return this;
+ }
+
+ public Response redirect(String location, int httpStatusCode) {
+ servletResponse.setStatus(httpStatusCode);
+ servletResponse.setHeader("Location", location);
+ servletResponse.setHeader("Connection", "close");
+ try {
+ servletResponse.sendError(httpStatusCode);
+ } catch (IOException e) {
+ log.warn("Exception while trying to redirect", e);
+ }
+ return this;
+ }
+
+ public int status() {
+ return servletResponse.getStatus();
+ }
+
+ public Response status(int statusCode) {
+ servletResponse.setStatus(statusCode);
+ return this;
+ }
+
+ // cookie methods
+
+ public Response cookie(String name, String value) {
+ return cookie(cookieBuilder(name, value));
+ }
+
+ public Response cookie(String name, String value, int maxAge) {
+ return cookie(cookieBuilder(name, value).maxAge(maxAge));
+ }
+
+ public Response cookie(CookieBuilder cookieBuilder) {
+ Cookie cookie = new Cookie(cookieBuilder.name(), cookieBuilder.value());
+ cookie.setPath(cookieBuilder.path());
+ cookie.setDomain(cookieBuilder.domain());
+ cookie.setMaxAge(cookieBuilder.maxAge());
+ cookie.setSecure(cookieBuilder.secure());
+ cookie.setHttpOnly(cookieBuilder.httpOnly());
+ servletResponse.addCookie(cookie);
+ return this;
+ }
+
+ public Response removeCookie(String name) {
+ return removeCookie(null, name);
+ }
+
+ public Response removeCookie(String path, String name) {
+ Cookie cookie = new Cookie(name, "");
+ cookie.setPath(path);
+ cookie.setMaxAge(0);
+ servletResponse.addCookie(cookie);
+ return this;
+ }
+
+}
diff --git a/src/main/java/javalin/ResponseMapper.java b/src/main/java/javalin/ResponseMapper.java
new file mode 100644
index 000000000..14be260f0
--- /dev/null
+++ b/src/main/java/javalin/ResponseMapper.java
@@ -0,0 +1,28 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin;
+
+import javalin.core.util.Util;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class ResponseMapper {
+
+ // TODO: Add GSON or other alternatives?
+
+ public static String toJson(Object object) {
+ if (Util.classExists("com.fasterxml.jackson.databind.ObjectMapper")) {
+ try {
+ return new ObjectMapper().writeValueAsString(object);
+ } catch (JsonProcessingException e) {
+ throw new HaltException(500, "Failed to write object as JSON");
+ }
+ } else {
+ throw new HaltException(500, "No JSON-mapper available");
+ }
+ }
+
+}
diff --git a/src/main/java/javalin/builder/CookieBuilder.java b/src/main/java/javalin/builder/CookieBuilder.java
new file mode 100644
index 000000000..e425d5ad4
--- /dev/null
+++ b/src/main/java/javalin/builder/CookieBuilder.java
@@ -0,0 +1,83 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.builder;
+
+import javalin.core.util.Util;
+
+public class CookieBuilder {
+
+ private String name;
+ private String value;
+ private String domain = "";
+ private String path = "";
+ private int maxAge = -1;
+ private boolean secure = false;
+ private boolean httpOnly = false;
+
+ public static CookieBuilder cookieBuilder(String name, String value) {
+ return new CookieBuilder(name, value);
+ }
+
+ private CookieBuilder(String name, String value) {
+ Util.notNull(name, "Cookie name cannot be null");
+ Util.notNull(value, "Cookie value cannot be null");
+ this.name = name;
+ this.value = value;
+ }
+
+ public CookieBuilder domain(String domain) {
+ this.domain = domain;
+ return this;
+ }
+
+ public CookieBuilder path(String path) {
+ this.path = path;
+ return this;
+ }
+
+ public CookieBuilder maxAge(int maxAge) {
+ this.maxAge = maxAge;
+ return this;
+ }
+
+ public CookieBuilder secure(boolean secure) {
+ this.secure = secure;
+ return this;
+ }
+
+ public CookieBuilder httpOnly(boolean httpOnly) {
+ this.httpOnly = httpOnly;
+ return this;
+ }
+
+ // getters
+ public String name() {
+ return name;
+ }
+
+ public String value() {
+ return value;
+ }
+
+ public String domain() {
+ return domain;
+ }
+
+ public String path() {
+ return path;
+ }
+
+ public int maxAge() {
+ return maxAge;
+ }
+
+ public boolean secure() {
+ return secure;
+ }
+
+ public boolean httpOnly() {
+ return httpOnly;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/javalin/core/ErrorMapper.java b/src/main/java/javalin/core/ErrorMapper.java
new file mode 100644
index 000000000..90d60f225
--- /dev/null
+++ b/src/main/java/javalin/core/ErrorMapper.java
@@ -0,0 +1,36 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.core;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javalin.ErrorHandler;
+import javalin.Request;
+import javalin.Response;
+
+public class ErrorMapper {
+
+ private Map errorHandlerMap;
+
+ public ErrorMapper() {
+ this.errorHandlerMap = new HashMap<>();
+ }
+
+ public void put(Integer statusCode, ErrorHandler handler) {
+ this.errorHandlerMap.put(statusCode, handler);
+ }
+
+ public void clear() {
+ this.errorHandlerMap.clear();
+ }
+
+ void handle(int statusCode, Request request, Response response) {
+ ErrorHandler errorHandler = errorHandlerMap.get(statusCode);
+ if (errorHandler != null) {
+ errorHandler.handle(request, response);
+ }
+ }
+}
diff --git a/src/main/java/javalin/core/ExceptionMapper.java b/src/main/java/javalin/core/ExceptionMapper.java
new file mode 100644
index 000000000..e150abc6f
--- /dev/null
+++ b/src/main/java/javalin/core/ExceptionMapper.java
@@ -0,0 +1,72 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.core;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javalin.ExceptionHandler;
+import javalin.HaltException;
+import javalin.Request;
+import javalin.Response;
+
+public class ExceptionMapper {
+
+ private static Logger log = LoggerFactory.getLogger(ExceptionMapper.class);
+
+ private Map, ExceptionHandler> exceptionMap;
+
+ public ExceptionMapper() {
+ this.exceptionMap = new HashMap<>();
+ }
+
+ public void put(Class extends Exception> exceptionClass, ExceptionHandler handler) {
+ this.exceptionMap.put(exceptionClass, handler);
+ }
+
+ public void clear() {
+ this.exceptionMap.clear();
+ }
+
+
+ void handle(Exception e, Request request, Response response) {
+ if (e instanceof HaltException) {
+ response.status(((HaltException) e).statusCode);
+ response.body(((HaltException) e).body);
+ return;
+ }
+ ExceptionHandler exceptionHandler = this.getHandler(e.getClass());
+ if (exceptionHandler != null) {
+ exceptionHandler.handle(e, request, response);
+ } else {
+ log.error("Uncaught exception", e);
+ response.body("Internal server error");
+ response.status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+
+
+ public ExceptionHandler getHandler(Class extends Exception> exceptionClass) {
+ if (this.exceptionMap.containsKey(exceptionClass)) {
+ return this.exceptionMap.get(exceptionClass);
+ }
+ Class> superclass = exceptionClass.getSuperclass();
+ while (superclass != null) {
+ if (this.exceptionMap.containsKey(superclass)) {
+ ExceptionHandler matchingHandler = this.exceptionMap.get(superclass);
+ this.exceptionMap.put(exceptionClass, matchingHandler); // superclass was found, avoid search next time
+ return matchingHandler;
+ }
+ superclass = superclass.getSuperclass();
+ }
+ this.exceptionMap.put(exceptionClass, null); // nothing was found, avoid search next time
+ return null;
+ }
+}
diff --git a/src/main/java/javalin/core/HandlerEntry.java b/src/main/java/javalin/core/HandlerEntry.java
new file mode 100644
index 000000000..a1ad619b1
--- /dev/null
+++ b/src/main/java/javalin/core/HandlerEntry.java
@@ -0,0 +1,21 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.core;
+
+import javalin.Handler;
+
+public class HandlerEntry {
+
+ public Handler.Type type;
+ public String path;
+ public Handler handler;
+
+ public HandlerEntry(Handler.Type type, String path, Handler handler) {
+ this.type = type;
+ this.path = path;
+ this.handler = handler;
+ }
+
+}
diff --git a/src/main/java/javalin/core/HandlerMatch.java b/src/main/java/javalin/core/HandlerMatch.java
new file mode 100644
index 000000000..93caeb07c
--- /dev/null
+++ b/src/main/java/javalin/core/HandlerMatch.java
@@ -0,0 +1,20 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.core;
+
+import javalin.Handler;
+
+public class HandlerMatch {
+
+ public Handler handler;
+ public String handlerUri;
+ public String requestUri;
+
+ public HandlerMatch(Handler handler, String matchUri, String requestUri) {
+ this.handler = handler;
+ this.handlerUri = matchUri;
+ this.requestUri = requestUri;
+ }
+}
diff --git a/src/main/java/javalin/core/JavalinServlet.java b/src/main/java/javalin/core/JavalinServlet.java
new file mode 100644
index 000000000..4baed0c4d
--- /dev/null
+++ b/src/main/java/javalin/core/JavalinServlet.java
@@ -0,0 +1,123 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.core;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import javalin.HaltException;
+import javalin.Handler;
+import javalin.Request;
+import javalin.Response;
+import javalin.core.util.RequestUtil;
+import javalin.embeddedserver.StaticResourceHandler;
+
+public class JavalinServlet implements Servlet {
+
+ private PathMatcher pathMatcher;
+ private ExceptionMapper exceptionMapper;
+ private ErrorMapper errorMapper;
+ private StaticResourceHandler staticResourceHandler;
+
+ public JavalinServlet(PathMatcher pathMatcher, ExceptionMapper exceptionMapper, ErrorMapper errorMapper, StaticResourceHandler staticResourceHandler) {
+ this.pathMatcher = pathMatcher;
+ this.exceptionMapper = exceptionMapper;
+ this.errorMapper = errorMapper;
+ this.staticResourceHandler = staticResourceHandler;
+ }
+
+ @Override
+ public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
+
+ HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
+ HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
+ Handler.Type type = Handler.Type.fromServletRequest(httpRequest);
+ String requestUri = httpRequest.getRequestURI();
+ Request request = RequestUtil.create(httpRequest);
+ Response response = new Response(httpResponse);
+
+ response.header("Server", "Javalin");
+
+ try { // before-handlers, endpoint-handlers, static-files
+
+ for (HandlerMatch beforeHandler : pathMatcher.findFilterHandlers(Handler.Type.BEFORE, requestUri)) {
+ beforeHandler.handler.handle(RequestUtil.create(httpRequest, beforeHandler), response);
+ }
+
+ HandlerMatch routeHandler = pathMatcher.findEndpointHandler(type, requestUri);
+ if (routeHandler != null && routeHandler.handler != null) {
+ routeHandler.handler.handle(RequestUtil.create(httpRequest, routeHandler), response);
+ } else if (type != Handler.Type.HEAD || (type == Handler.Type.HEAD && pathMatcher.findEndpointHandler(Handler.Type.GET, requestUri) == null)) {
+ if (staticResourceHandler.handle(httpRequest, httpResponse)) {
+ return;
+ }
+ throw new HaltException(404, "Not found");
+ }
+
+ } catch (Exception e) {
+ // both before-handlers and endpoint-handlers can throw Exception,
+ // we need to handle those here in order to run after-filters even if an exception was thrown
+ exceptionMapper.handle(e, request, response);
+ }
+
+ try { // after-handlers
+ for (HandlerMatch afterHandler : pathMatcher.findFilterHandlers(Handler.Type.AFTER, requestUri)) {
+ afterHandler.handler.handle(RequestUtil.create(httpRequest, afterHandler), response);
+ }
+ } catch (Exception e) {
+ // after filters can also throw exceptions
+ exceptionMapper.handle(e, request, response);
+ }
+
+ try { // error mapping (turning status codes into standardized messages/pages)
+ errorMapper.handle(response.status(), request, response);
+ } catch (RuntimeException e) {
+ // depressingly, the error mapping itself could throw a runtime exception
+ // we need to handle these last... but that's it.
+ exceptionMapper.handle(e, request, response);
+ }
+
+ // javalin is done doing stuff, write result to servlet-response
+ if (response.contentType() == null) {
+ httpResponse.setContentType("text/plain");
+ }
+ if (response.encoding() == null) {
+ httpResponse.setCharacterEncoding(StandardCharsets.UTF_8.name());
+ }
+ if (response.body() != null) {
+ httpResponse.getWriter().write(response.body());
+ httpResponse.getWriter().flush();
+ httpResponse.getWriter().close();
+ }
+
+ }
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ }
+
+ @Override
+ public ServletConfig getServletConfig() {
+ return null;
+ }
+
+ @Override
+ public String getServletInfo() {
+ return null;
+ }
+
+ @Override
+ public void destroy() {
+ }
+
+}
diff --git a/src/main/java/javalin/core/PathMatcher.java b/src/main/java/javalin/core/PathMatcher.java
new file mode 100644
index 000000000..f9027afb6
--- /dev/null
+++ b/src/main/java/javalin/core/PathMatcher.java
@@ -0,0 +1,107 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.core;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javalin.Handler;
+import javalin.core.util.Util;
+
+public class PathMatcher {
+
+ private List handlerEntries;
+
+ public PathMatcher() {
+ handlerEntries = new ArrayList<>();
+ }
+
+ public void add(Handler.Type type, String path, Handler handler) {
+ handlerEntries.add(new HandlerEntry(type, path, handler));
+ }
+
+ public void clear() {
+ handlerEntries.clear();
+ }
+
+ public HandlerMatch findEndpointHandler(Handler.Type type, String path) {
+ List handlerEntries = findTargetsForRequestedHandler(type, path);
+ if (handlerEntries.size() == 0) {
+ return null;
+ }
+ HandlerEntry entry = handlerEntries.get(0);
+ return new HandlerMatch(entry.handler, entry.path, path);
+ }
+
+ public List findFilterHandlers(Handler.Type type, String path) {
+ return findTargetsForRequestedHandler(type, path).stream()
+ .map(handlerEntry -> new HandlerMatch(handlerEntry.handler, handlerEntry.path, path))
+ .collect(Collectors.toList());
+ }
+
+ private List findTargetsForRequestedHandler(Handler.Type type, String path) {
+ return handlerEntries.stream()
+ .filter(r -> match(r, type, path))
+ .collect(Collectors.toList());
+ }
+
+ // TODO: Consider optimizing this
+ private static boolean match(HandlerEntry handlerEntry, Handler.Type requestType, String requestPath) {
+ if (handlerEntry.type != requestType) {
+ return false;
+ }
+ if (endingSlashesDoNotMatch(handlerEntry.path, requestPath)) {
+ return false;
+ }
+ if (handlerEntry.path.equals(requestPath)) { // identical paths
+ return true;
+ }
+ return matchParamAndWildcard(handlerEntry.path, requestPath);
+ }
+
+ private static boolean matchParamAndWildcard(String handlerPath, String requestPath) {
+
+ List handlerPaths = Util.pathToList(handlerPath);
+ List requestPaths = Util.pathToList(requestPath);
+
+ int numHandlerPaths = handlerPaths.size();
+ int numRequestPaths = requestPaths.size();
+
+ if (numHandlerPaths == numRequestPaths) {
+ for (int i = 0; i < numHandlerPaths; i++) {
+ String handlerPathPart = handlerPaths.get(i);
+ String requestPathPart = requestPaths.get(i);
+ if (handlerPathPart.equals("*") && handlerPath.endsWith("*") && (i == numHandlerPaths - 1)) {
+ return true;
+ }
+ if (!handlerPathPart.equals("*") && !handlerPathPart.startsWith(":") && !handlerPathPart.equals(requestPathPart)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ if (handlerPath.endsWith("*") && numHandlerPaths < numRequestPaths) {
+ for (int i = 0; i < numHandlerPaths; i++) {
+ String handlerPathPart = handlerPaths.get(i);
+ String requestPathPart = requestPaths.get(i);
+ if (handlerPathPart.equals("*") && handlerPath.endsWith("*") && (i == numHandlerPaths - 1)) {
+ return true;
+ }
+ if (!handlerPathPart.startsWith(":") && !handlerPathPart.equals("*") && !handlerPathPart.equals(requestPathPart)) {
+ return false;
+ }
+ }
+ return false;
+ }
+ return false;
+ }
+
+ private static boolean endingSlashesDoNotMatch(String handlerPath, String requestPath) {
+ return requestPath.endsWith("/") && !handlerPath.endsWith("/")
+ || !requestPath.endsWith("/") && handlerPath.endsWith("/");
+ }
+
+}
diff --git a/src/main/java/javalin/core/util/RequestUtil.java b/src/main/java/javalin/core/util/RequestUtil.java
new file mode 100644
index 000000000..22306ef7e
--- /dev/null
+++ b/src/main/java/javalin/core/util/RequestUtil.java
@@ -0,0 +1,110 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.core.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import javalin.Request;
+import javalin.core.HandlerMatch;
+
+public class RequestUtil {
+
+ public static Request create(HttpServletRequest httpRequest) {
+ return new Request(httpRequest, new HashMap<>(), new ArrayList<>());
+ }
+
+ public static Request create(HttpServletRequest httpRequest, HandlerMatch handlerMatch) {
+ List requestList = Util.pathToList(handlerMatch.requestUri);
+ List matchedList = Util.pathToList(handlerMatch.handlerUri);
+ return new Request(
+ httpRequest,
+ getParams(requestList, matchedList),
+ getSplat(requestList, matchedList)
+ );
+ }
+
+ public static List getSplat(List request, List matched) {
+ int numRequestParts = request.size();
+ int numMatchedParts = matched.size();
+ boolean sameLength = (numRequestParts == numMatchedParts);
+ List splat = new ArrayList<>();
+ for (int i = 0; (i < numRequestParts) && (i < numMatchedParts); i++) {
+ String matchedPart = matched.get(i);
+ if (isSplat(matchedPart)) {
+ StringBuilder splatParam = new StringBuilder(request.get(i));
+ if (!sameLength && (i == (numMatchedParts - 1))) {
+ for (int j = i + 1; j < numRequestParts; j++) {
+ splatParam.append("/");
+ splatParam.append(request.get(j));
+ }
+ }
+ splat.add(urlDecode(splatParam.toString()));
+ }
+ }
+ return Collections.unmodifiableList(splat);
+ }
+
+ public static Map getParams(List requestPaths, List handlerPaths) {
+ Map params = new HashMap<>();
+ for (int i = 0; (i < requestPaths.size()) && (i < handlerPaths.size()); i++) {
+ String matchedPart = handlerPaths.get(i);
+ if (isParam(matchedPart)) {
+ params.put(matchedPart.toLowerCase(), urlDecode(requestPaths.get(i)));
+ }
+ }
+ return Collections.unmodifiableMap(params);
+ }
+
+ public static String urlDecode(String s) {
+ try {
+ return URLDecoder.decode(s.replace("+", "%2B"), "UTF-8").replace("%2B", "+");
+ } catch (UnsupportedEncodingException ignored) {
+ return "";
+ }
+ }
+
+ public static boolean isParam(String pathPart) {
+ return pathPart.startsWith(":");
+ }
+
+ public static boolean isSplat(String pathPart) {
+ return pathPart.equals("*");
+ }
+
+ public static String byteArrayToString(byte[] bytes, String encoding) {
+ String string;
+ if (encoding != null && Charset.isSupported(encoding)) {
+ try {
+ string = new String(bytes, encoding);
+ } catch (UnsupportedEncodingException e) {
+ string = new String(bytes);
+ }
+ } else {
+ string = new String(bytes);
+ }
+ return string;
+ }
+
+ public static byte[] toByteArray(InputStream input) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] byteBuffer = new byte[1024];
+ for (int b = input.read(byteBuffer); b != -1; b = input.read(byteBuffer)) {
+ baos.write(byteBuffer, 0, b);
+ }
+ return baos.toByteArray();
+ }
+}
diff --git a/src/main/java/javalin/core/util/Util.java b/src/main/java/javalin/core/util/Util.java
new file mode 100644
index 000000000..1dd05b8be
--- /dev/null
+++ b/src/main/java/javalin/core/util/Util.java
@@ -0,0 +1,50 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.core.util;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public class Util {
+
+ private Util() {
+ }
+
+ public static void notNull(Object object, String message) {
+ if (object == null) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ public static boolean classExists(String className) {
+ try {
+ Class.forName(className);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public static List pathToList(String pathString) {
+ return Stream.of(pathString.split("/"))
+ .filter(p -> p.length() > 0)
+ .collect(Collectors.toList());
+ }
+
+ public static String javalinBanner() {
+ return " _________________________________________\n"
+ + "| _ _ _ |\n"
+ + "| | | __ ___ ____ _| (_)_ __ |\n"
+ + "| _ | |/ _` \\ \\ / / _` | | | '_ \\ |\n"
+ + "| | |_| | (_| |\\ V / (_| | | | | | | |\n"
+ + "| \\___/ \\__,_| \\_/ \\__,_|_|_|_| |_| |\n"
+ + "|_________________________________________|\n"
+ + "| |\n"
+ + "| https://javalin.io/documentation |\n"
+ + "|_________________________________________|\n";
+ }
+
+}
diff --git a/src/main/java/javalin/embeddedserver/CachedRequestWrapper.java b/src/main/java/javalin/embeddedserver/CachedRequestWrapper.java
new file mode 100644
index 000000000..f092bc1ae
--- /dev/null
+++ b/src/main/java/javalin/embeddedserver/CachedRequestWrapper.java
@@ -0,0 +1,69 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.embeddedserver;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+
+import javalin.core.util.RequestUtil;
+
+public class CachedRequestWrapper extends HttpServletRequestWrapper {
+
+ private byte[] cachedBytes;
+
+ public CachedRequestWrapper(HttpServletRequest request) throws IOException {
+ super(request);
+ cachedBytes = RequestUtil.toByteArray(super.getInputStream());
+ }
+
+ @Override
+ public ServletInputStream getInputStream() throws IOException {
+ if (chunkedTransferEncoding()) { // this could blow up memory if cached
+ return super.getInputStream();
+ }
+ return new CachedServletInputStream();
+ }
+
+ private boolean chunkedTransferEncoding() {
+ return "chunked".equals(((HttpServletRequest) super.getRequest()).getHeader("Transfer-Encoding"));
+ }
+
+ private class CachedServletInputStream extends ServletInputStream {
+ private ByteArrayInputStream byteArrayInputStream;
+
+ public CachedServletInputStream() {
+ this.byteArrayInputStream = new ByteArrayInputStream(cachedBytes);
+ }
+
+ @Override
+ public int read() {
+ return byteArrayInputStream.read();
+ }
+
+ @Override
+ public int available() {
+ return byteArrayInputStream.available();
+ }
+
+ @Override
+ public boolean isFinished() {
+ return available() <= 0;
+ }
+
+ @Override
+ public boolean isReady() {
+ return available() >= 0;
+ }
+
+ @Override
+ public void setReadListener(ReadListener readListener) {
+ }
+ }
+}
diff --git a/src/main/java/javalin/embeddedserver/EmbeddedServer.java b/src/main/java/javalin/embeddedserver/EmbeddedServer.java
new file mode 100644
index 000000000..334c5a7dd
--- /dev/null
+++ b/src/main/java/javalin/embeddedserver/EmbeddedServer.java
@@ -0,0 +1,18 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.embeddedserver;
+
+public interface EmbeddedServer {
+
+ int start(String host, int port) throws Exception;
+
+ void join() throws InterruptedException;
+
+ void stop();
+
+ int activeThreadCount();
+
+ Object attribute(String key);
+}
diff --git a/src/main/java/javalin/embeddedserver/EmbeddedServerFactory.java b/src/main/java/javalin/embeddedserver/EmbeddedServerFactory.java
new file mode 100644
index 000000000..13b7136f0
--- /dev/null
+++ b/src/main/java/javalin/embeddedserver/EmbeddedServerFactory.java
@@ -0,0 +1,13 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.embeddedserver;
+
+import javalin.core.ErrorMapper;
+import javalin.core.ExceptionMapper;
+import javalin.core.PathMatcher;
+
+public interface EmbeddedServerFactory {
+ EmbeddedServer create(PathMatcher pathMatcher, ExceptionMapper exceptionMapper, ErrorMapper errorMapper, String staticFileDirectory);
+}
diff --git a/src/main/java/javalin/embeddedserver/StaticResourceHandler.java b/src/main/java/javalin/embeddedserver/StaticResourceHandler.java
new file mode 100644
index 000000000..9ad27c668
--- /dev/null
+++ b/src/main/java/javalin/embeddedserver/StaticResourceHandler.java
@@ -0,0 +1,13 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.embeddedserver;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public interface StaticResourceHandler {
+ // should return if request has been handled
+ boolean handle(HttpServletRequest httpRequest, HttpServletResponse httpResponse);
+}
diff --git a/src/main/java/javalin/embeddedserver/jetty/EmbeddedJettyFactory.java b/src/main/java/javalin/embeddedserver/jetty/EmbeddedJettyFactory.java
new file mode 100644
index 000000000..647842f82
--- /dev/null
+++ b/src/main/java/javalin/embeddedserver/jetty/EmbeddedJettyFactory.java
@@ -0,0 +1,50 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.embeddedserver.jetty;
+
+import java.util.concurrent.TimeUnit;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+
+import javalin.core.ErrorMapper;
+import javalin.core.ExceptionMapper;
+import javalin.core.JavalinServlet;
+import javalin.core.PathMatcher;
+import javalin.core.util.Util;
+import javalin.embeddedserver.EmbeddedServer;
+import javalin.embeddedserver.EmbeddedServerFactory;
+
+public class EmbeddedJettyFactory implements EmbeddedServerFactory {
+
+ private JettyServer jettyServer;
+
+ public EmbeddedJettyFactory() {
+ this.jettyServer = () -> new Server(new QueuedThreadPool(200, 8, 60_000));
+ }
+
+ public EmbeddedJettyFactory(JettyServer jettyServer) {
+ this.jettyServer = jettyServer;
+ }
+
+ public EmbeddedServer create(PathMatcher pathMatcher, ExceptionMapper exceptionMapper, ErrorMapper errorMapper, String staticFileDirectory) {
+ JettyResourceHandler resourceHandler = new JettyResourceHandler(staticFileDirectory);
+ JavalinServlet javalinServlet = new JavalinServlet(pathMatcher, exceptionMapper, errorMapper, resourceHandler);
+ return new EmbeddedJettyServer(jettyServer, new JettyHandler(javalinServlet));
+ }
+
+ public static ServerConnector defaultConnector(Server server, String host, int port) {
+ Util.notNull(server, "server cannot be null");
+ Util.notNull(host, "host cannot be null");
+ ServerConnector connector = new ServerConnector(server);
+ connector.setIdleTimeout(TimeUnit.HOURS.toMillis(1));
+ connector.setSoLingerTime(-1);
+ connector.setHost(host);
+ connector.setPort(port);
+ return connector;
+ }
+
+}
diff --git a/src/main/java/javalin/embeddedserver/jetty/EmbeddedJettyServer.java b/src/main/java/javalin/embeddedserver/jetty/EmbeddedJettyServer.java
new file mode 100644
index 000000000..33c73e427
--- /dev/null
+++ b/src/main/java/javalin/embeddedserver/jetty/EmbeddedJettyServer.java
@@ -0,0 +1,99 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.embeddedserver.jetty;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javalin.Javalin;
+import javalin.embeddedserver.EmbeddedServer;
+
+public class EmbeddedJettyServer implements EmbeddedServer {
+
+ private JettyServer jettyServer;
+ private Server server;
+ private SessionHandler javalinHandler;
+
+ private static Logger log = LoggerFactory.getLogger(EmbeddedServer.class);
+
+ public EmbeddedJettyServer(JettyServer jettyServer, SessionHandler javalinHandler) {
+ this.jettyServer = jettyServer;
+ this.javalinHandler = javalinHandler;
+ }
+
+ @Override
+ public int start(String host, int port) throws Exception {
+
+ if (port == 0) {
+ try (ServerSocket serverSocket = new ServerSocket(0)) {
+ port = serverSocket.getLocalPort();
+ } catch (IOException e) {
+ log.error("Failed to get first available port, using default port instead: " + Javalin.DEFAULT_PORT);
+ port = Javalin.DEFAULT_PORT;
+ }
+ }
+
+ server = jettyServer.create();
+
+ if (server.getConnectors().length == 0) {
+ ServerConnector serverConnector = EmbeddedJettyFactory.defaultConnector(server, host, port);
+ server = serverConnector.getServer();
+ server.setConnectors(new Connector[] {serverConnector});
+ }
+
+ server.setHandler(javalinHandler);
+ server.start();
+
+ log.info("Javalin has started \\o/");
+ for (Connector connector : server.getConnectors()) {
+ log.info("Localhost: " + getProtocol(connector) + "://localhost:" + ((ServerConnector) connector).getLocalPort());
+ }
+
+ return ((ServerConnector) server.getConnectors()[0]).getLocalPort();
+ }
+
+ private static String getProtocol(Connector connector) {
+ return connector.getProtocols().contains("ssl") ? "https" : "http";
+ }
+
+ @Override
+ public void join() throws InterruptedException {
+ server.join();
+ }
+
+ @Override
+ public void stop() {
+ log.info("Stopping Javalin ...");
+ try {
+ if (server != null) {
+ server.stop();
+ }
+ } catch (Exception e) {
+ log.error("Javalin failed to stop gracefully, calling System.exit()", e);
+ System.exit(100);
+ }
+ log.info("Javalin stopped");
+ }
+
+ @Override
+ public int activeThreadCount() {
+ if (server == null) {
+ return 0;
+ }
+ return server.getThreadPool().getThreads() - server.getThreadPool().getIdleThreads();
+ }
+
+ @Override
+ public Object attribute(String key) {
+ return server.getAttribute(key);
+ }
+}
diff --git a/src/main/java/javalin/embeddedserver/jetty/JettyHandler.java b/src/main/java/javalin/embeddedserver/jetty/JettyHandler.java
new file mode 100644
index 000000000..6f9cbbf9b
--- /dev/null
+++ b/src/main/java/javalin/embeddedserver/jetty/JettyHandler.java
@@ -0,0 +1,36 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.embeddedserver.jetty;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.session.SessionHandler;
+
+import javalin.core.JavalinServlet;
+import javalin.embeddedserver.CachedRequestWrapper;
+
+public class JettyHandler extends SessionHandler {
+
+ private JavalinServlet javalinServlet;
+
+ public JettyHandler(JavalinServlet javalinServlet) {
+ this.javalinServlet = javalinServlet;
+ }
+
+ @Override
+ public void doHandle(String target, Request jettyRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+ CachedRequestWrapper cachedRequest = new CachedRequestWrapper(request);
+ cachedRequest.setAttribute("jetty-target", target);
+ cachedRequest.setAttribute("jetty-request", jettyRequest);
+ javalinServlet.service(cachedRequest, response);
+ jettyRequest.setHandled(true);
+ }
+
+}
diff --git a/src/main/java/javalin/embeddedserver/jetty/JettyResourceHandler.java b/src/main/java/javalin/embeddedserver/jetty/JettyResourceHandler.java
new file mode 100644
index 000000000..bbcd44eeb
--- /dev/null
+++ b/src/main/java/javalin/embeddedserver/jetty/JettyResourceHandler.java
@@ -0,0 +1,60 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.embeddedserver.jetty;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.ResourceHandler;
+import org.eclipse.jetty.util.resource.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javalin.embeddedserver.StaticResourceHandler;
+
+public class JettyResourceHandler implements StaticResourceHandler {
+
+ private static Logger log = LoggerFactory.getLogger(JettyResourceHandler.class);
+
+ private boolean initialized = false;
+ private ResourceHandler resourceHandler = new ResourceHandler();
+
+ public JettyResourceHandler(String staticFileDirectory) {
+ if (staticFileDirectory != null) {
+ resourceHandler.setResourceBase(Resource.newClassPathResource(staticFileDirectory).toString());
+ resourceHandler.setDirAllowed(false);
+ resourceHandler.setEtags(true);
+ try {
+ resourceHandler.start();
+ initialized = true;
+ } catch (Exception e) {
+ log.error("Exception occurred starting static resource handler", e);
+ }
+ }
+ }
+
+ public boolean handle(HttpServletRequest request, HttpServletResponse response) {
+ if (initialized) {
+ String target = (String) request.getAttribute("jetty-target");
+ Request baseRequest = (Request) request.getAttribute("jetty-request");
+ try {
+ if (!resourceHandler.getResource(target).isDirectory()) {
+ resourceHandler.handle(target, baseRequest, request, response);
+ } else if (resourceHandler.getResource(target + "index.html").exists()) {
+ resourceHandler.handle(target, baseRequest, request, response);
+ }
+ } catch (IOException | ServletException e) {
+ log.error("Exception occurred while handling static resource", e);
+ }
+ return baseRequest.isHandled();
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/javalin/embeddedserver/jetty/JettyServer.java b/src/main/java/javalin/embeddedserver/jetty/JettyServer.java
new file mode 100644
index 000000000..4a19e3e7f
--- /dev/null
+++ b/src/main/java/javalin/embeddedserver/jetty/JettyServer.java
@@ -0,0 +1,12 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.embeddedserver.jetty;
+
+import org.eclipse.jetty.server.Server;
+
+@FunctionalInterface
+public interface JettyServer {
+ Server create();
+}
diff --git a/src/main/java/javalin/lifecycle/Event.java b/src/main/java/javalin/lifecycle/Event.java
new file mode 100644
index 000000000..feba424cb
--- /dev/null
+++ b/src/main/java/javalin/lifecycle/Event.java
@@ -0,0 +1,31 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.lifecycle;
+
+import javalin.Javalin;
+
+public class Event {
+
+ public enum Type {
+ SERVER_STARTING,
+ SERVER_STARTED,
+ SERVER_STOPPING,
+ SERVER_STOPPED
+ }
+
+ public Type eventType;
+ public Javalin javalin;
+
+ public Event(Type eventType) {
+ this.eventType = eventType;
+ this.javalin = null;
+ }
+
+ public Event(Type eventType, Javalin javalin) {
+ this.eventType = eventType;
+ this.javalin = javalin;
+ }
+
+}
diff --git a/src/main/java/javalin/lifecycle/EventListener.java b/src/main/java/javalin/lifecycle/EventListener.java
new file mode 100644
index 000000000..8b4297b0f
--- /dev/null
+++ b/src/main/java/javalin/lifecycle/EventListener.java
@@ -0,0 +1,10 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.lifecycle;
+
+@FunctionalInterface
+public interface EventListener {
+ void handleEvent(Event e);
+}
diff --git a/src/main/java/javalin/lifecycle/EventManager.java b/src/main/java/javalin/lifecycle/EventManager.java
new file mode 100644
index 000000000..e329f604c
--- /dev/null
+++ b/src/main/java/javalin/lifecycle/EventManager.java
@@ -0,0 +1,35 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.lifecycle;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javalin.Javalin;
+
+public class EventManager {
+
+ private Map> listenerMap;
+
+ public EventManager() {
+ this.listenerMap = Stream.of(Event.Type.values()).collect(Collectors.toMap(v -> v, v -> new LinkedList<>()));
+ }
+
+ public synchronized void addEventListener(Event.Type type, EventListener listener) {
+ listenerMap.get(type).add(listener);
+ }
+
+ public void fireEvent(Event.Type type, Javalin javalin) {
+ listenerMap.get(type).forEach(listener -> listener.handleEvent(new Event(type, javalin)));
+ }
+
+ public void fireEvent(Event.Type type) {
+ fireEvent(type, null);
+ }
+
+}
diff --git a/src/main/java/javalin/security/AccessManager.java b/src/main/java/javalin/security/AccessManager.java
new file mode 100644
index 000000000..bbf87c3ce
--- /dev/null
+++ b/src/main/java/javalin/security/AccessManager.java
@@ -0,0 +1,16 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.security;
+
+import java.util.List;
+
+import javalin.Handler;
+import javalin.Request;
+import javalin.Response;
+
+@FunctionalInterface
+public interface AccessManager {
+ void manage(Handler handler, Request request, Response response, List permittedRoles) throws Exception;
+}
\ No newline at end of file
diff --git a/src/main/java/javalin/security/Role.java b/src/main/java/javalin/security/Role.java
new file mode 100644
index 000000000..66820ac50
--- /dev/null
+++ b/src/main/java/javalin/security/Role.java
@@ -0,0 +1,14 @@
+// Javalin - https://javalin.io
+// Copyright 2017 David Åse
+// Licensed under Apache 2.0: https://github.com/tipsy/javalin/blob/master/LICENSE
+
+package javalin.security;
+
+import java.util.Arrays;
+import java.util.List;
+
+public interface Role {
+ static List roles(Role... roles) {
+ return Arrays.asList(roles);
+ }
+}
diff --git a/src/test/java/javalin/TestAccessManager.java b/src/test/java/javalin/TestAccessManager.java
new file mode 100644
index 000000000..c35c1b0b2
--- /dev/null
+++ b/src/test/java/javalin/TestAccessManager.java
@@ -0,0 +1,70 @@
+package javalin;
+
+import org.junit.Test;
+
+import javalin.security.AccessManager;
+import javalin.security.Role;
+
+import com.mashape.unirest.http.Unirest;
+import com.mashape.unirest.http.exceptions.UnirestException;
+
+import static javalin.ApiBuilder.*;
+import static javalin.TestAccessManager.MyRoles.*;
+import static javalin.security.Role.roles;
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestAccessManager {
+
+ private AccessManager accessManager = (handler, request, response, permittedRoles) -> {
+ String userRole = request.queryParam("role");
+ if (userRole != null && permittedRoles.contains(MyRoles.valueOf(userRole))) {
+ handler.handle(request, response);
+ } else {
+ response.status(401).body("Unauthorized");
+ }
+ };
+
+ enum MyRoles implements Role {
+ ROLE_ONE, ROLE_TWO, ROLE_THREE;
+ }
+
+ static String origin = "http://localhost:1234";
+
+ @Test
+ public void test_noAccessManager_throwsException() throws Exception {
+ Javalin app = Javalin.create().port(1234).start().awaitInitialization();
+ app.get("/secured", (req, res) -> res.body("Hello"), roles(ROLE_ONE));
+ assertThat(callWithRole("/secured", "ROLE_ONE"), is("Internal server error"));
+ app.stop().awaitTermination();
+ }
+
+ @Test
+ public void test_accessManager_restrictsAccess() throws Exception {
+ Javalin app = Javalin.create().port(1234).start().awaitInitialization();
+ app.accessManager(accessManager);
+ app.get("/secured", (req, res) -> res.body("Hello"), roles(ROLE_ONE, ROLE_TWO));
+ assertThat(callWithRole("/secured", "ROLE_ONE"), is("Hello"));
+ assertThat(callWithRole("/secured", "ROLE_TWO"), is("Hello"));
+ assertThat(callWithRole("/secured", "ROLE_THREE"), is("Unauthorized"));
+ app.stop().awaitTermination();
+ }
+
+ @Test
+ public void test_accessManager_restrictsAccess_forStaticApi() throws Exception {
+ Javalin app = Javalin.create().port(1234).start().awaitInitialization();
+ app.accessManager(accessManager);
+ app.routes(() -> {
+ get("/static-secured", (req, res) -> res.body("Hello"), roles(ROLE_ONE, ROLE_TWO));
+ });
+ assertThat(callWithRole("/static-secured", "ROLE_ONE"), is("Hello"));
+ assertThat(callWithRole("/static-secured", "ROLE_TWO"), is("Hello"));
+ assertThat(callWithRole("/static-secured", "ROLE_THREE"), is("Unauthorized"));
+ app.stop().awaitTermination();
+ }
+
+ private String callWithRole(String path, String role) throws UnirestException {
+ return Unirest.get(origin + path).queryString("role", role).asString().getBody();
+ }
+
+}
diff --git a/src/test/java/javalin/TestApiBuilder.java b/src/test/java/javalin/TestApiBuilder.java
new file mode 100644
index 000000000..e108df886
--- /dev/null
+++ b/src/test/java/javalin/TestApiBuilder.java
@@ -0,0 +1,58 @@
+package javalin;
+
+import org.junit.Test;
+
+import static javalin.ApiBuilder.*;
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestApiBuilder extends _UnirestBaseTest {
+
+ @Test
+ public void test_pathWorks_forGet() throws Exception {
+ app.routes(() -> {
+ get("/hello", simpleAnswer("Hello from level 0"));
+ path("/level-1", () -> {
+ get("/hello", simpleAnswer("Hello from level 1"));
+ get("/hello-2", simpleAnswer("Hello again from level 1"));
+ post("/create-1", simpleAnswer("Created something at level 1"));
+ path("/level-2", () -> {
+ get("/hello", simpleAnswer("Hello from level 2"));
+ path("/level-3", () -> {
+ get("/hello", simpleAnswer("Hello from level 3"));
+ });
+ });
+ });
+ });
+ assertThat(GET_body("/hello"), is("Hello from level 0"));
+ assertThat(GET_body("/level-1/hello"), is("Hello from level 1"));
+ assertThat(GET_body("/level-1/level-2/hello"), is("Hello from level 2"));
+ assertThat(GET_body("/level-1/level-2/level-3/hello"), is("Hello from level 3"));
+ }
+
+ private Handler simpleAnswer(String body) {
+ return (req, res) -> res.body(body);
+ }
+
+ @Test
+ public void test_pathWorks_forFilters() throws Exception {
+ app.routes(() -> {
+ path("/level-1", () -> {
+ before("/*", (req, res) -> res.body("1"));
+ path("/level-2", () -> {
+ path("/level-3", () -> {
+ get("/hello", updateAnswer("Hello"));
+ });
+ after("/*", updateAnswer("2"));
+ });
+ });
+ });
+ assertThat(GET_body("/level-1/level-2/level-3/hello"), is("1Hello2"));
+ }
+
+ private Handler updateAnswer(String body) {
+ return (req, res) -> res.body(res.body() + body);
+ }
+
+}
+
diff --git a/src/test/java/javalin/TestApiBuilderTwoServices.java b/src/test/java/javalin/TestApiBuilderTwoServices.java
new file mode 100644
index 000000000..d5c731b45
--- /dev/null
+++ b/src/test/java/javalin/TestApiBuilderTwoServices.java
@@ -0,0 +1,42 @@
+package javalin;
+
+import org.junit.Test;
+
+import com.mashape.unirest.http.Unirest;
+import com.mashape.unirest.http.exceptions.UnirestException;
+
+import static javalin.ApiBuilder.*;
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.*;
+
+public class TestApiBuilderTwoServices {
+
+ @Test
+ public void testApiBuilder_twoServices() throws Exception {
+ Javalin app1 = Javalin.create().port(55555).start().awaitInitialization();
+ Javalin app2 = Javalin.create().port(55556).start().awaitInitialization();
+ app1.routes(() -> {
+ get("/hello1", (req, res) -> res.body("Hello1"));
+ });
+ app2.routes(() -> {
+ get("/hello1", (req, res) -> res.body("Hello1"));
+ });
+ app1.routes(() -> {
+ get("/hello2", (req, res) -> res.body("Hello2"));
+ });
+ app2.routes(() -> {
+ get("/hello2", (req, res) -> res.body("Hello2"));
+ });
+ assertThat(call(55555, "/hello1"), is("Hello1"));
+ assertThat(call(55556, "/hello1"), is("Hello1"));
+ assertThat(call(55555, "/hello2"), is("Hello2"));
+ assertThat(call(55556, "/hello2"), is("Hello2"));
+ app1.stop().awaitTermination();
+ app2.stop().awaitTermination();
+ }
+
+ private String call(int port, String path) throws UnirestException {
+ return Unirest.get("http://localhost:" + port + path).asString().getBody();
+ }
+
+}
diff --git a/src/test/java/javalin/TestBodyReading.java b/src/test/java/javalin/TestBodyReading.java
new file mode 100644
index 000000000..cdc6062ef
--- /dev/null
+++ b/src/test/java/javalin/TestBodyReading.java
@@ -0,0 +1,70 @@
+package javalin;
+
+import org.junit.Test;
+
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.Unirest;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestBodyReading {
+
+ @Test
+ public void test_bodyReader() throws Exception {
+ Javalin app = Javalin.create().port(0).start().awaitInitialization();
+ app.before("/body-reader", (req, res) -> res.header("X-BEFORE", req.body() + req.queryParam("qp")));
+ app.post("/body-reader", (req, res) -> res.body(req.body() + req.queryParam("qp")));
+ app.after("/body-reader", (req, res) -> res.header("X-AFTER", req.body() + req.queryParam("qp")));
+
+ HttpResponse response = Unirest
+ .post("http://localhost:" + app.port() + "/body-reader")
+ .queryString("qp", "queryparam")
+ .body("body")
+ .asString();
+
+ assertThat(response.getHeaders().getFirst("X-BEFORE"), is("bodyqueryparam"));
+ assertThat(response.getBody(), is("bodyqueryparam"));
+ assertThat(response.getHeaders().getFirst("X-AFTER"), is("bodyqueryparam"));
+ app.stop().awaitTermination();
+ }
+
+ @Test
+ public void test_bodyReader_reverse() throws Exception {
+ Javalin app = Javalin.create().port(0).start().awaitInitialization();
+ app.before("/body-reader", (req, res) -> res.header("X-BEFORE", req.queryParam("qp") + req.body()));
+ app.post("/body-reader", (req, res) -> res.body(req.queryParam("qp") + req.body()));
+ app.after("/body-reader", (req, res) -> res.header("X-AFTER", req.queryParam("qp") + req.body()));
+
+ HttpResponse response = Unirest
+ .post("http://localhost:" + app.port() + "/body-reader")
+ .queryString("qp", "queryparam")
+ .body("body")
+ .asString();
+
+ assertThat(response.getHeaders().getFirst("X-BEFORE"), is("queryparambody"));
+ assertThat(response.getBody(), is("queryparambody"));
+ assertThat(response.getHeaders().getFirst("X-AFTER"), is("queryparambody"));
+ app.stop().awaitTermination();
+ }
+
+ @Test
+ public void test_formParams_work() throws Exception {
+ Javalin app = Javalin.create().port(0).start().awaitInitialization();
+ app.before("/body-reader", (req, res) -> res.header("X-BEFORE", req.bodyParam("username")));
+ app.post("/body-reader", (req, res) -> res.body(req.bodyParam("password")));
+ app.after("/body-reader", (req, res) -> res.header("X-AFTER", req.bodyParam("repeat-password")));
+
+ HttpResponse response = Unirest
+ .post("http://localhost:" + app.port() + "/body-reader")
+ .body("username=some-user-name&password=password&repeat-password=password")
+ .asString();
+
+ assertThat(response.getHeaders().getFirst("X-BEFORE"), is("some-user-name"));
+ assertThat(response.getBody(), is("password"));
+ assertThat(response.getHeaders().getFirst("X-AFTER"), is("password"));
+ app.stop().awaitTermination();
+ }
+
+
+}
diff --git a/src/test/java/javalin/TestCustomJetty.java b/src/test/java/javalin/TestCustomJetty.java
new file mode 100644
index 000000000..9dc46c4ca
--- /dev/null
+++ b/src/test/java/javalin/TestCustomJetty.java
@@ -0,0 +1,29 @@
+package javalin;
+
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.junit.Test;
+
+import javalin.embeddedserver.jetty.EmbeddedJettyFactory;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.*;
+
+public class TestCustomJetty {
+
+ @Test
+ public void test_embeddedServer_setsCustomServer() throws Exception {
+ Javalin app = Javalin.create()
+ .port(0)
+ .embeddedServer(new EmbeddedJettyFactory(() -> {
+ Server server = new Server(new QueuedThreadPool(200, 8, 60_000));
+ server.setAttribute("is-custom-server", true);
+ return server;
+ }))
+ .start()
+ .awaitInitialization();
+ assertThat(app.embeddedServer().attribute("is-custom-server"), is(true));
+ app.stop().awaitTermination();
+ }
+
+}
diff --git a/src/test/java/javalin/TestEncoding.java b/src/test/java/javalin/TestEncoding.java
new file mode 100644
index 000000000..ad66d63d6
--- /dev/null
+++ b/src/test/java/javalin/TestEncoding.java
@@ -0,0 +1,35 @@
+package javalin;
+
+import java.net.URLEncoder;
+
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestEncoding extends _UnirestBaseTest {
+
+ @Test
+ public void test_param_unicode() throws Exception {
+ app.get("/:param", (req, res) -> res.body(req.param("param")));
+ assertThat(GET_body("/æøå"), is("æøå"));
+ assertThat(GET_body("/♚♛♜♜♝♝♞♞♟♟♟♟♟♟♟♟"), is("♚♛♜♜♝♝♞♞♟♟♟♟♟♟♟♟"));
+ assertThat(GET_body("/こんにちは"), is("こんにちは"));
+ }
+
+ @Test
+ public void test_queryParam_unicode() throws Exception {
+ app.get("/", (req, res) -> res.body(req.queryParam("qp")));
+ assertThat(GET_body("/?qp=æøå"), is("æøå"));
+ assertThat(GET_body("/?qp=♚♛♜♜♝♝♞♞♟♟♟♟♟♟♟♟"), is("♚♛♜♜♝♝♞♞♟♟♟♟♟♟♟♟"));
+ assertThat(GET_body("/?qp=こんにちは"), is("こんにちは"));
+ }
+
+ @Test
+ public void test_queryParam_encoded() throws Exception {
+ app.get("/", (req, res) -> res.body(req.queryParam("qp")));
+ String encoded = URLEncoder.encode("!#$&'()*+,/:;=?@[]", "UTF-8");
+ assertThat(GET_body("/?qp=" + encoded), is("!#$&'()*+,/:;=?@[]"));
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/javalin/TestErrorMapper.java b/src/test/java/javalin/TestErrorMapper.java
new file mode 100644
index 000000000..41af74624
--- /dev/null
+++ b/src/test/java/javalin/TestErrorMapper.java
@@ -0,0 +1,53 @@
+package javalin;
+
+import org.junit.Test;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestErrorMapper extends _UnirestBaseTest {
+
+ @Test
+ public void test_404mapper_works() throws Exception {
+ app.error(404, (req, res) -> {
+ res.body("Custom 404 page");
+ });
+ assertThat(GET_body("/unmapped"), is("Custom 404 page"));
+ }
+
+ @Test
+ public void test_500mapper_works() throws Exception {
+ app.get("/exception", (req, res) -> {
+ throw new RuntimeException();
+ }).error(500, (req, res) -> {
+ res.body("Custom 500 page");
+ });
+ assertThat(GET_body("/exception"), is("Custom 500 page"));
+ }
+
+ @Test
+ public void testError_higherPriority_thanException() throws Exception {
+ app.get("/exception", (req, res) -> {
+ throw new RuntimeException();
+ }).exception(Exception.class, (e, req, res) -> {
+ res.status(500).body("Exception handled!");
+ }).error(500, (req, res) -> {
+ res.body("Custom 500 page");
+ });
+ assertThat(GET_body("/exception"), is("Custom 500 page"));
+ }
+
+ @Test
+ public void testError_throwingException_isCaughtByExceptionMapper() throws Exception {
+ app.get("/exception", (req, res) -> {
+ throw new RuntimeException();
+ }).exception(Exception.class, (e, req, res) -> {
+ res.status(500).body("Exception handled!");
+ }).error(500, (req, res) -> {
+ res.body("Custom 500 page");
+ throw new RuntimeException();
+ });
+ assertThat(GET_body("/exception"), is("Exception handled!"));
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/javalin/TestExceptionMapper.java b/src/test/java/javalin/TestExceptionMapper.java
new file mode 100644
index 000000000..c17271d47
--- /dev/null
+++ b/src/test/java/javalin/TestExceptionMapper.java
@@ -0,0 +1,60 @@
+package javalin;
+
+import org.junit.Test;
+
+import javalin.util.TypedException;
+
+import com.mashape.unirest.http.HttpResponse;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestExceptionMapper extends _UnirestBaseTest {
+
+ @Test
+ public void test_unmappedException_caughtByGeneralHandler() throws Exception {
+ app.get("/unmapped-exception", (req, res) -> {
+ throw new Exception();
+ });
+ HttpResponse response = GET_asString("/unmapped-exception");
+ assertThat(response.getBody(), is("Internal server error"));
+ assertThat(response.getStatus(), is(500));
+ }
+
+ @Test
+ public void test_mappedException_isHandled() throws Exception {
+ app.get("/mapped-exception", (req, res) -> {
+ throw new Exception();
+ }).exception(Exception.class, (e, req, res) -> res.body("It's been handled."));
+ HttpResponse response = GET_asString("/mapped-exception");
+ assertThat(response.getBody(), is("It's been handled."));
+ assertThat(response.getStatus(), is(200));
+ }
+
+ @Test
+ public void test_typedMappedException_isHandled() throws Exception {
+ app.get("/typed-exception", (req, res) -> {
+ throw new TypedException();
+ }).exception(TypedException.class, (e, req, res) -> {
+ res.body(e.proofOfType());
+ });
+ HttpResponse response = GET_asString("/typed-exception");
+ assertThat(response.getBody(), is("I'm so typed"));
+ assertThat(response.getStatus(), is(200));
+ }
+
+ @Test
+ public void test_moreSpecificException_isHandledFirst() throws Exception {
+ app.get("/exception-priority", (req, res) -> {
+ throw new TypedException();
+ }).exception(Exception.class, (e, req, res) -> {
+ res.body("This shouldn't run");
+ }).exception(TypedException.class, (e, req, res) -> {
+ res.body(e.proofOfType());
+ });
+ HttpResponse response = GET_asString("/exception-priority");
+ assertThat(response.getBody(), is("I'm so typed"));
+ assertThat(response.getStatus(), is(200));
+ }
+
+}
diff --git a/src/test/java/javalin/TestFilters.java b/src/test/java/javalin/TestFilters.java
new file mode 100644
index 000000000..90cfdd899
--- /dev/null
+++ b/src/test/java/javalin/TestFilters.java
@@ -0,0 +1,77 @@
+package javalin;
+
+import org.junit.Test;
+
+import com.mashape.unirest.http.HttpMethod;
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.exceptions.UnirestException;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestFilters extends _UnirestBaseTest {
+
+ @Test
+ public void test_justFilters_is404() throws Exception {
+ Handler emptyHandler = (req, res) -> {
+ };
+ app.before(emptyHandler);
+ app.after(emptyHandler);
+ HttpResponse response = call(HttpMethod.GET, "/hello");
+ assertThat(response.getStatus(), is(404));
+ assertThat(response.getBody(), is("Not found"));
+ }
+
+ @Test
+ public void test_beforeFilter_setsHeader() throws Exception {
+ app.before((req, res) -> res.header("X-FILTER", "Before-filter ran"));
+ app.get("/mapped", OK_HANDLER);
+ assertThat(getAndGetHeader("/mapped", "X-FILTER"), is("Before-filter ran"));
+ }
+
+ @Test
+ public void test_multipleFilters_setHeaders() throws Exception {
+ app.before((req, res) -> res.header("X-FILTER-1", "Before-filter 1 ran"));
+ app.before((req, res) -> res.header("X-FILTER-2", "Before-filter 2 ran"));
+ app.before((req, res) -> res.header("X-FILTER-3", "Before-filter 3 ran"));
+ app.after((req, res) -> res.header("X-FILTER-4", "After-filter 1 ran"));
+ app.after((req, res) -> res.header("X-FILTER-5", "After-filter 2 ran"));
+ app.get("/mapped", OK_HANDLER);
+ assertThat(getAndGetHeader("/mapped", "X-FILTER-1"), is("Before-filter 1 ran"));
+ assertThat(getAndGetHeader("/mapped", "X-FILTER-2"), is("Before-filter 2 ran"));
+ assertThat(getAndGetHeader("/mapped", "X-FILTER-3"), is("Before-filter 3 ran"));
+ assertThat(getAndGetHeader("/mapped", "X-FILTER-4"), is("After-filter 1 ran"));
+ assertThat(getAndGetHeader("/mapped", "X-FILTER-5"), is("After-filter 2 ran"));
+ }
+
+ @Test
+ public void test_afterFilter_setsHeader() throws Exception {
+ app.after((req, res) -> res.header("X-FILTER", "After-filter ran"));
+ app.get("/mapped", OK_HANDLER);
+ assertThat(getAndGetHeader("/mapped", "X-FILTER"), is("After-filter ran"));
+ }
+
+ @Test
+ public void test_afterFilter_overrides_beforeFilter() throws Exception {
+ app.before((req, res) -> res.header("X-FILTER", "This header is mine!"));
+ app.after((req, res) -> res.header("X-FILTER", "After-filter beats before-filter"));
+ app.get("/mapped", OK_HANDLER);
+ assertThat(getAndGetHeader("/mapped", "X-FILTER"), is("After-filter beats before-filter"));
+ }
+
+ @Test
+ public void test_beforeFilter_canAddTrailingSlashes() throws Exception {
+ app.before((Request request, Response response) -> {
+ if (!request.path().endsWith("/")) {
+ response.redirect(request.path() + "/");
+ }
+ });
+ app.get("/ok/", OK_HANDLER);
+ assertThat(GET_body("/ok"), is("OK"));
+ }
+
+ private String getAndGetHeader(String path, String header) throws UnirestException {
+ return call(HttpMethod.GET, path).getHeaders().getFirst(header);
+ }
+
+}
diff --git a/src/test/java/javalin/TestGetPortAfterRandomPortInit.java b/src/test/java/javalin/TestGetPortAfterRandomPortInit.java
new file mode 100644
index 000000000..dab511b19
--- /dev/null
+++ b/src/test/java/javalin/TestGetPortAfterRandomPortInit.java
@@ -0,0 +1,39 @@
+package javalin;
+
+import java.io.IOException;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.mashape.unirest.http.Unirest;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestGetPortAfterRandomPortInit {
+
+ private static Javalin app;
+ private static int port;
+
+ @BeforeClass
+ public static void setup() throws IOException {
+ app = Javalin.create()
+ .port(0)
+ .start()
+ .awaitInitialization();
+ port = app.port();
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ app.stop();
+ }
+
+ @Test
+ public void test_get_helloWorld() throws Exception {
+ app.get("/hello", (req, res) -> res.body("Hello World"));
+ assertThat(Unirest.get("http://localhost:" + port + "/hello").asString().getStatus(), is(200));
+ }
+
+}
diff --git a/src/test/java/javalin/TestHaltException.java b/src/test/java/javalin/TestHaltException.java
new file mode 100644
index 000000000..dda103197
--- /dev/null
+++ b/src/test/java/javalin/TestHaltException.java
@@ -0,0 +1,46 @@
+package javalin;
+
+import org.junit.Test;
+
+import com.mashape.unirest.http.HttpMethod;
+import com.mashape.unirest.http.HttpResponse;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestHaltException extends _UnirestBaseTest {
+
+ @Test
+ public void test_haltBeforeWildcard_works() throws Exception {
+ app.before("/admin/*", (req, res) -> {
+ throw new HaltException(401);
+ });
+ app.get("/admin/protected", (req, res) -> res.body("Protected resource"));
+ HttpResponse response = call(HttpMethod.GET, "/admin/protected");
+ assertThat(response.getStatus(), is(401));
+ assertThat(response.getBody(), not("Protected resource"));
+ }
+
+ @Test
+ public void test_haltInRoute_works() throws Exception {
+ app.get("/some-route", (req, res) -> {
+ throw new HaltException(401, "Stop!");
+ });
+ HttpResponse response = call(HttpMethod.GET, "/some-route");
+ assertThat(response.getBody(), is("Stop!"));
+ assertThat(response.getStatus(), is(401));
+ }
+
+ @Test
+ public void test_afterRuns_afterHalt() throws Exception {
+ app.get("/some-route", (req, res) -> {
+ throw new HaltException(401, "Stop!");
+ }).after((req, res) -> {
+ res.status(418);
+ });
+ HttpResponse response = call(HttpMethod.GET, "/some-route");
+ assertThat(response.getBody(), is("Stop!"));
+ assertThat(response.getStatus(), is(418));
+ }
+
+}
diff --git a/src/test/java/javalin/TestHttpVerbs.java b/src/test/java/javalin/TestHttpVerbs.java
new file mode 100644
index 000000000..b39555d21
--- /dev/null
+++ b/src/test/java/javalin/TestHttpVerbs.java
@@ -0,0 +1,58 @@
+package javalin;
+
+import org.junit.Test;
+
+import com.mashape.unirest.http.HttpMethod;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestHttpVerbs extends _UnirestBaseTest {
+
+ @Test
+ public void test_get_helloWorld() throws Exception {
+ app.get("/hello", (req, res) -> res.body("Hello World"));
+ assertThat(GET_body("/hello"), is("Hello World"));
+ }
+
+ @Test
+ public void test_get_helloOtherWorld() throws Exception {
+ app.get("/hello", (req, res) -> res.body("Hello New World"));
+ assertThat(GET_body("/hello"), is("Hello New World"));
+ }
+
+ @Test
+ public void test_all_mapped_verbs_ok() throws Exception {
+ app.get("/mapped", OK_HANDLER);
+ app.post("/mapped", OK_HANDLER);
+ app.put("/mapped", OK_HANDLER);
+ app.delete("/mapped", OK_HANDLER);
+ app.patch("/mapped", OK_HANDLER);
+ app.head("/mapped", OK_HANDLER);
+ app.options("/mapped", OK_HANDLER);
+ for (HttpMethod httpMethod : HttpMethod.values()) {
+ assertThat(call(httpMethod, "/mapped").getStatus(), is(200));
+ }
+ }
+
+ @Test
+ public void test_all_unmapped_verbs_ok() throws Exception {
+ for (HttpMethod httpMethod : HttpMethod.values()) {
+ assertThat(call(httpMethod, "/unmapped").getStatus(), is(404));
+ }
+ }
+
+ @Test
+ public void test_all_nonMapped_verbs_404() throws Exception {
+ for (HttpMethod httpMethod : HttpMethod.values()) {
+ assertThat(call(httpMethod, "/not-mapped").getStatus(), is(404));
+ }
+ }
+
+ @Test
+ public void test_headOk_ifGetMapped() throws Exception {
+ app.get("/mapped", OK_HANDLER);
+ assertThat(call(HttpMethod.HEAD, "/mapped").getStatus(), is(200));
+ }
+
+}
diff --git a/src/test/java/javalin/TestInitExceptionHandler.java b/src/test/java/javalin/TestInitExceptionHandler.java
new file mode 100644
index 000000000..b4c82a7df
--- /dev/null
+++ b/src/test/java/javalin/TestInitExceptionHandler.java
@@ -0,0 +1,36 @@
+package javalin;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static javalin.Javalin.*;
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.*;
+
+public class TestInitExceptionHandler {
+
+ private static int NON_VALID_PORT = Integer.MAX_VALUE;
+ private static Javalin app;
+ private static String errorMessage = "Override me!";
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ app = create()
+ .port(NON_VALID_PORT)
+ .startupExceptionHandler((e) -> errorMessage = "Woops...")
+ .start()
+ .awaitInitialization();
+ }
+
+ @Test
+ public void testInitExceptionHandler() throws Exception {
+ assertThat(errorMessage, is("Woops..."));
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ app.stop();
+ }
+
+}
diff --git a/src/test/java/javalin/TestLifecycleEvents.java b/src/test/java/javalin/TestLifecycleEvents.java
new file mode 100644
index 000000000..527a3a6ac
--- /dev/null
+++ b/src/test/java/javalin/TestLifecycleEvents.java
@@ -0,0 +1,34 @@
+package javalin;
+
+import org.junit.Test;
+
+import javalin.lifecycle.Event;
+
+import static org.hamcrest.CoreMatchers.*;
+import static org.hamcrest.MatcherAssert.*;
+
+public class TestLifecycleEvents {
+
+ private static String startingMsg = "";
+ private static String startedMsg = "";
+ private static String stoppingMsg = "";
+ private static String stoppedMsg = "";
+
+ @Test
+ public void testLifecycleEvents() {
+ Javalin.create()
+ .event(Event.Type.SERVER_STARTING, e -> startingMsg = "Starting")
+ .event(Event.Type.SERVER_STARTED, e -> startedMsg = "Started")
+ .event(Event.Type.SERVER_STOPPING, e -> stoppingMsg = "Stopping")
+ .event(Event.Type.SERVER_STOPPED, e -> stoppedMsg = "Stopped")
+ .start()
+ .awaitInitialization()
+ .stop()
+ .awaitTermination();
+ assertThat(startingMsg, is("Starting"));
+ assertThat(startedMsg, is("Started"));
+ assertThat(stoppingMsg, is("Stopping"));
+ assertThat(stoppedMsg, is("Stopped"));
+ }
+
+}
diff --git a/src/test/java/javalin/TestMultipleInstances.java b/src/test/java/javalin/TestMultipleInstances.java
new file mode 100644
index 000000000..cfa299a7a
--- /dev/null
+++ b/src/test/java/javalin/TestMultipleInstances.java
@@ -0,0 +1,49 @@
+package javalin;
+
+import java.io.IOException;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.mashape.unirest.http.Unirest;
+import com.mashape.unirest.http.exceptions.UnirestException;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestMultipleInstances {
+
+ private static Javalin app1;
+ private static Javalin app2;
+ private static Javalin app3;
+
+ @BeforeClass
+ public static void setup() throws IOException {
+ app1 = Javalin.create().port(7001).start().awaitInitialization();
+ app2 = Javalin.create().port(7002).start().awaitInitialization();
+ app3 = Javalin.create().port(7003).start().awaitInitialization();
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ app1.stop();
+ app2.stop();
+ app3.stop();
+ }
+
+ @Test
+ public void test_getMultiple() throws Exception {
+ app1.get("/hello-1", (req, res) -> res.body("Hello first World"));
+ app2.get("/hello-2", (req, res) -> res.body("Hello second World"));
+ app3.get("/hello-3", (req, res) -> res.body("Hello third World"));
+ assertThat(getBody("7001", "/hello-1"), is("Hello first World"));
+ assertThat(getBody("7002", "/hello-2"), is("Hello second World"));
+ assertThat(getBody("7003", "/hello-3"), is("Hello third World"));
+ }
+
+ static String getBody(String port, String pathname) throws UnirestException {
+ return Unirest.get("http://localhost:" + port + pathname).asString().getBody();
+ }
+
+}
diff --git a/src/test/java/javalin/TestRequest.java b/src/test/java/javalin/TestRequest.java
new file mode 100644
index 000000000..ddfbb8940
--- /dev/null
+++ b/src/test/java/javalin/TestRequest.java
@@ -0,0 +1,98 @@
+package javalin;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.Unirest;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestRequest extends _UnirestBaseTest {
+
+ /*
+ * Cookies
+ */
+ @Test
+ public void test_getSingleCookie_worksForMissingCookie() throws Exception {
+ app.get("/read-cookie", (req, res) -> res.body("" + req.cookie("my-cookie")));
+ assertThat(GET_body("/read-cookie"), is("null")); // notice {"" + req} on previous line
+ }
+
+ @Test
+ public void test_getSingleCookie_worksForCookie() throws Exception {
+ app.get("/read-cookie", (req, res) -> res.body(req.cookie("my-cookie")));
+ HttpResponse response = Unirest.get(origin + "/read-cookie").header("Cookie", "my-cookie=my-cookie-value").asString();
+ assertThat(response.getBody(), is("my-cookie-value"));
+ }
+
+ @Test
+ public void test_getMultipleCookies_worksForNoCookies() throws Exception {
+ app.get("/read-cookie", (req, res) -> res.body(req.cookieMap().toString()));
+ assertThat(GET_body("/read-cookie"), is("{}"));
+ }
+
+ @Test
+ public void test_getMultipleCookies_worksForMultipleCookies() throws Exception {
+ app.get("/read-cookie", (req, res) -> res.body(req.cookieMap().toString()));
+ HttpResponse response = Unirest.get(origin + "/read-cookie").header("Cookie", "k1=v1;k2=v2;k3=v3").asString();
+ assertThat(response.getBody(), is("{k1=v1, k2=v2, k3=v3}"));
+ }
+
+ /*
+ * Path params
+ */
+ @Test
+ public void test_paramWork_noParam() throws Exception {
+ app.get("/my/path", (req, res) -> res.body("" + req.param("param")));
+ assertThat(GET_body("/my/path"), is("null")); // notice {"" + req} on previous line
+ }
+
+ @Test
+ public void test_paramWork_multipleSingleParams() throws Exception {
+ app.get("/:1/:2/:3", (req, res) -> res.body(req.param("1") + req.param("2") + req.param("3")));
+ assertThat(GET_body("/my/path/params"), is("mypathparams"));
+ }
+
+ @Test
+ public void test_paramMapWorks_noParamsPresent() throws Exception {
+ app.get("/my/path/params", (req, res) -> res.body(req.paramMap().toString()));
+ assertThat(GET_body("/my/path/params"), is("{}"));
+ }
+
+ @Test
+ public void test_paramMapWorks_paramsPresent() throws Exception {
+ app.get("/:1/:2/:3", (req, res) -> res.body(req.paramMap().toString()));
+ assertThat(GET_body("/my/path/params"), is("{:1=my, :2=path, :3=params}"));
+ }
+
+ /*
+ * Query params
+ */
+ @Test
+ public void test_queryParamWorks_noParam() throws Exception {
+ app.get("/", (req, res) -> res.body("" + req.queryParam("qp")));
+ assertThat(GET_body("/"), is("null")); // notice {"" + req} on previous line
+ }
+
+ @Test
+ public void test_queryParamWorks_multipleSingleParams() throws Exception {
+ app.get("/", (req, res) -> res.body(req.queryParam("qp1") + req.queryParam("qp2") + req.queryParam("qp3")));
+ assertThat(GET_body("/?qp1=1&qp2=2&qp3=3"), is("123"));
+ }
+
+ @Test
+ public void test_queryParamsWorks_noParamsPresent() throws Exception {
+ app.get("/", (req, res) -> res.body(Arrays.toString(req.queryParams("qp1"))));
+ assertThat(GET_body("/"), is("null")); // notice {"" + req} on previous line
+ }
+
+ @Test
+ public void test_queryParamsWorks_paramsPresent() throws Exception {
+ app.get("/", (req, res) -> res.body(Arrays.toString(req.queryParams("qp1"))));
+ assertThat(GET_body("/?qp1=1&qp1=2&qp1=3"), is("[1, 2, 3]")); // notice {"" + req} on previous line
+ }
+
+}
diff --git a/src/test/java/javalin/TestResponse.java b/src/test/java/javalin/TestResponse.java
new file mode 100644
index 000000000..095977852
--- /dev/null
+++ b/src/test/java/javalin/TestResponse.java
@@ -0,0 +1,91 @@
+package javalin;
+
+import java.util.List;
+
+import org.junit.Test;
+
+import javalin.util.TestObject_NonSerializable;
+import javalin.util.TestObject_Serializable;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.mashape.unirest.http.HttpMethod;
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.Unirest;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestResponse extends _UnirestBaseTest {
+
+ private String MY_BODY = ""
+ + "This is my body, and I live in it. It's 31 and 6 months old. "
+ + "It's changed a lot since it was new. It's done stuff it wasn't built to do. "
+ + "I often try to fill if up with wine. - Tim Minchin";
+
+ @Test
+ public void test_responseBuilder() throws Exception {
+ app.get("/hello", (req, res) ->
+ res.status(418)
+ .body(MY_BODY)
+ .header("X-HEADER-1", "my-header-1")
+ .header("X-HEADER-2", "my-header-2"));
+ HttpResponse response = call(HttpMethod.GET, "/hello");
+ assertThat(response.getStatus(), is(418));
+ assertThat(response.getBody(), is(MY_BODY));
+ assertThat(response.getHeaders().getFirst("X-HEADER-1"), is("my-header-1"));
+ assertThat(response.getHeaders().getFirst("X-HEADER-2"), is("my-header-2"));
+ }
+
+ @Test
+ public void test_responseBuilder_json() throws Exception {
+ app.get("/hello", (req, res) -> res.status(200).json(new TestObject_Serializable()));
+ String expected = new ObjectMapper().writeValueAsString(new TestObject_Serializable());
+ assertThat(GET_body("/hello"), is(expected));
+ }
+
+ @Test
+ public void test_responseBuilder_json_haltsForBadObject() throws Exception {
+ app.get("/hello", (req, res) -> res.status(200).json(new TestObject_NonSerializable()));
+ HttpResponse response = call(HttpMethod.GET, "/hello");
+ assertThat(response.getStatus(), is(500));
+ assertThat(response.getBody(), is("Failed to write object as JSON"));
+ }
+
+ @Test
+ public void test_redirect() throws Exception {
+ app.get("/hello", (req, res) -> res.redirect("/hello-2"));
+ app.get("/hello-2", (req, res) -> res.body("Redirected"));
+ assertThat(GET_body("/hello"), is("Redirected"));
+ }
+
+ @Test
+ public void test_redirectWithStatus() throws Exception {
+ app.get("/hello", (req, res) -> res.redirect("/hello-2", 302));
+ app.get("/hello-2", (req, res) -> res.body("Redirected"));
+ Unirest.setHttpClient(noRedirectClient); // disable redirects
+ HttpResponse response = call(HttpMethod.GET, "/hello");
+ assertThat(response.getStatus(), is(302));
+ Unirest.setHttpClient(defaultHttpClient); // re-enable redirects
+ response = call(HttpMethod.GET, "/hello");
+ assertThat(response.getBody(), is("Redirected"));
+ }
+
+ @Test
+ public void test_createCookie() throws Exception {
+ app.post("/create-cookies", (req, res) -> res.cookie("name1", "value1").cookie("name2", "value2"));
+ HttpResponse response = call(HttpMethod.POST, "/create-cookies");
+ List cookies = response.getHeaders().get("Set-Cookie");
+ assertThat(cookies, hasItem("name1=value1"));
+ assertThat(cookies, hasItem("name2=value2"));
+ }
+
+ @Test
+ public void test_deleteCookie() throws Exception {
+ app.post("/create-cookie", (req, res) -> res.cookie("name1", "value1"));
+ app.post("/delete-cookie", (req, res) -> res.removeCookie("name1"));
+ HttpResponse response = call(HttpMethod.POST, "/create-cookies");
+ List cookies = response.getHeaders().get("Set-Cookie");
+ assertThat(cookies, is(nullValue()));
+ }
+
+}
diff --git a/src/test/java/javalin/TestRouting.java b/src/test/java/javalin/TestRouting.java
new file mode 100644
index 000000000..4ac5fff24
--- /dev/null
+++ b/src/test/java/javalin/TestRouting.java
@@ -0,0 +1,78 @@
+package javalin;
+
+import java.net.URLEncoder;
+
+import org.junit.Test;
+
+import javalin.util.SimpleHttpClient.TestResponse;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+public class TestRouting extends _SimpleClientBaseTest {
+
+ @Test
+ public void test_aBunchOfRoutes() throws Exception {
+ app.get("/", (req, res) -> res.body("/"));
+ app.get("/path", (req, res) -> res.body("/path"));
+ app.get("/path/:param", (req, res) -> res.body("/path/" + req.param("param")));
+ app.get("/path/:param/*", (req, res) -> res.body("/path/" + req.param("param") + "/" + req.splat(0)));
+ app.get("/*/*", (req, res) -> res.body("/" + req.splat(0) + "/" + req.splat(1)));
+ app.get("/*/unreachable", (req, res) -> res.body("reached"));
+ app.get("/*/*/:param", (req, res) -> res.body("/" + req.splat(0) + "/" + req.splat(1) + "/" + req.param("param")));
+ app.get("/*/*/:param/*", (req, res) -> res.body("/" + req.splat(0) + "/" + req.splat(1) + "/" + req.param("param") + "/" + req.splat(2)));
+
+ assertThat(simpleHttpClient.http_GET(origin + "/").body, is("/"));
+ assertThat(simpleHttpClient.http_GET(origin + "/path").body, is("/path"));
+ assertThat(simpleHttpClient.http_GET(origin + "/path/p").body, is("/path/p"));
+ assertThat(simpleHttpClient.http_GET(origin + "/path/p/s").body, is("/path/p/s"));
+ assertThat(simpleHttpClient.http_GET(origin + "/s1/s2").body, is("/s1/s2"));
+ assertThat(simpleHttpClient.http_GET(origin + "/s/unreachable").body, not("reached"));
+ assertThat(simpleHttpClient.http_GET(origin + "/s1/s2/p").body, is("/s1/s2/p"));
+ assertThat(simpleHttpClient.http_GET(origin + "/s1/s2/p/s3").body, is("/s1/s2/p/s3"));
+ assertThat(simpleHttpClient.http_GET(origin + "/s/s/s/s").body, is("/s/s/s/s"));
+ }
+
+
+ @Test
+ public void test_paramAndSplat() throws Exception {
+ app.get("/:param/path/*", (req, res) -> res.body(req.param("param") + req.splat(0)));
+ TestResponse response = simpleHttpClient.http_GET(origin + "/param/path/splat");
+ assertThat(response.body, is("paramsplat"));
+ }
+
+ @Test
+ public void test_encodedParam() throws Exception {
+ app.get("/:param", (req, res) -> res.body(req.param("param")));
+ String paramValue = "te/st";
+ TestResponse response = simpleHttpClient.http_GET(origin + "/" + URLEncoder.encode(paramValue, "UTF-8"));
+ assertThat(response.body, is(paramValue));
+ }
+
+ @Test
+ public void test_encdedParamAndEncodedSplat() throws Exception {
+ app.get("/:param/path/*", (req, res) -> res.body(req.param("param") + req.splat(0)));
+ TestResponse response = simpleHttpClient.http_GET(
+ origin + "/"
+ + URLEncoder.encode("java/kotlin", "UTF-8")
+ + "/path/"
+ + URLEncoder.encode("/java/kotlin", "UTF-8")
+ );
+ assertThat(response.body, is("java/kotlin/java/kotlin"));
+ }
+
+ @Test
+ public void test_caseSensitive_paramName() throws Exception {
+ app.get("/:ParaM", (req, res) -> res.body(req.param("pArAm")));
+ TestResponse response = simpleHttpClient.http_GET(origin + "/param");
+ assertThat(response.body, is("param"));
+ }
+
+ @Test
+ public void test_caseSensitive_paramValue() throws Exception {
+ app.get("/:param", (req, res) -> res.body(req.param("param")));
+ TestResponse response = simpleHttpClient.http_GET(origin + "/SomeCamelCasedValue");
+ assertThat(response.body, is("SomeCamelCasedValue"));
+ }
+
+}
diff --git a/src/test/java/javalin/TestStartStop.java b/src/test/java/javalin/TestStartStop.java
new file mode 100644
index 000000000..1d38893df
--- /dev/null
+++ b/src/test/java/javalin/TestStartStop.java
@@ -0,0 +1,23 @@
+package javalin;
+
+import org.junit.Test;
+
+
+public class TestStartStop {
+
+ @Test
+ public void test_waitsWorks_whenCalledInCorrectOrder() throws Exception {
+ Javalin.create().start().awaitInitialization().stop().awaitTermination();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void test_awaitInitThrowsException_whenNotStarted() throws Exception {
+ Javalin.create().awaitInitialization();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void test_awaitTerminationThrowsException_whenNotStopped() throws Exception {
+ Javalin.create().awaitTermination();
+ }
+
+}
diff --git a/src/test/java/javalin/TestStaticFiles.java b/src/test/java/javalin/TestStaticFiles.java
new file mode 100644
index 000000000..238c1d60c
--- /dev/null
+++ b/src/test/java/javalin/TestStaticFiles.java
@@ -0,0 +1,89 @@
+package javalin;
+
+import java.io.IOException;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.Unirest;
+
+import static org.hamcrest.MatcherAssert.*;
+import static org.hamcrest.Matchers.*;
+
+
+public class TestStaticFiles {
+
+ private static Javalin app;
+ private static String origin = "http://localhost:7777";
+
+ @BeforeClass
+ public static void setup() throws IOException {
+ app = Javalin.create()
+ .port(7777)
+ .enableStaticFiles("/public")
+ .start()
+ .awaitInitialization();
+ }
+
+ @After
+ public void clearRoutes() {
+ app.exceptionMapper.clear();
+ app.pathMatcher.clear();
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ app.stop();
+ app.awaitTermination();
+ }
+
+ @Test
+ public void test_Html() throws Exception {
+ HttpResponse response = Unirest.get(origin + "/html.html").asString();
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getBody(), containsString("HTML works"));
+
+ }
+
+ @Test
+ public void test_getJs() throws Exception {
+ HttpResponse response = Unirest.get(origin + "/script.js").asString();
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getBody(), containsString("JavaScript works"));
+ }
+
+ @Test
+ public void test_getCss() throws Exception {
+ HttpResponse response = Unirest.get(origin + "/styles.css").asString();
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getBody(), containsString("CSS works"));
+ }
+
+ @Test
+ public void test_beforeFilter() throws Exception {
+ app.before("/protected/*", (request, response) -> {
+ throw new HaltException(401, "Protected");
+ });
+ HttpResponse response = Unirest.get(origin + "/protected/secret.html").asString();
+ assertThat(response.getStatus(), is(401));
+ assertThat(response.getBody(), is("Protected"));
+ }
+
+ @Test
+ public void test_rootReturns404_ifNoWelcomeFile() throws Exception {
+ HttpResponse response = Unirest.get(origin + "/").asString();
+ assertThat(response.getStatus(), is(404));
+ assertThat(response.getBody(), is("Not found"));
+ }
+
+ @Test
+ public void test_rootReturnsWelcomeFile_ifWelcomeFileExists() throws Exception {
+ HttpResponse response = Unirest.get(origin + "/subdir/").asString();
+ assertThat(response.getStatus(), is(200));
+ assertThat(response.getBody(), is("Welcome file "));
+ }
+
+}
diff --git a/src/test/java/javalin/_SimpleClientBaseTest.java b/src/test/java/javalin/_SimpleClientBaseTest.java
new file mode 100644
index 000000000..712d6685a
--- /dev/null
+++ b/src/test/java/javalin/_SimpleClientBaseTest.java
@@ -0,0 +1,39 @@
+package javalin;
+
+import java.io.IOException;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import javalin.util.SimpleHttpClient;
+
+public class _SimpleClientBaseTest {
+
+ static Javalin app;
+ static String origin = "http://localhost:7777";
+
+ static SimpleHttpClient simpleHttpClient;
+
+ @BeforeClass
+ public static void setup() throws IOException {
+ app = Javalin.create()
+ .port(7777)
+ .start()
+ .awaitInitialization();
+ simpleHttpClient = new SimpleHttpClient();
+ }
+
+ @After
+ public void clearRoutes() {
+ app.errorMapper.clear();
+ app.exceptionMapper.clear();
+ app.pathMatcher.clear();
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ app.stop();
+ app.awaitTermination();
+ }
+}
diff --git a/src/test/java/javalin/_UnirestBaseTest.java b/src/test/java/javalin/_UnirestBaseTest.java
new file mode 100644
index 000000000..41527be30
--- /dev/null
+++ b/src/test/java/javalin/_UnirestBaseTest.java
@@ -0,0 +1,60 @@
+package javalin;
+
+import java.io.IOException;
+
+import org.apache.http.client.HttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+import com.mashape.unirest.http.HttpMethod;
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.Unirest;
+import com.mashape.unirest.http.exceptions.UnirestException;
+import com.mashape.unirest.request.HttpRequestWithBody;
+
+public class _UnirestBaseTest {
+
+ static Handler OK_HANDLER = (req, res) -> res.body("OK");
+
+ static Javalin app;
+ static String origin = "http://localhost:7777";
+
+ static HttpClient defaultHttpClient = HttpClients.custom().build();
+ static HttpClient noRedirectClient = HttpClients.custom().disableRedirectHandling().build();
+
+ @BeforeClass
+ public static void setup() throws IOException {
+ app = Javalin.create()
+ .port(7777)
+ .start()
+ .awaitInitialization();
+ }
+
+ @After
+ public void clearRoutes() {
+ app.errorMapper.clear();
+ app.exceptionMapper.clear();
+ app.pathMatcher.clear();
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ app.stop();
+ app.awaitTermination();
+ }
+
+ static String GET_body(String pathname) throws UnirestException {
+ return Unirest.get(origin + pathname).asString().getBody();
+ }
+
+ static HttpResponse GET_asString(String pathname) throws UnirestException {
+ return Unirest.get(origin + pathname).asString();
+ }
+
+ static HttpResponse call(HttpMethod method, String pathname) throws UnirestException {
+ return new HttpRequestWithBody(method, origin + pathname).asString();
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/javalin/examples/HelloWorld.java b/src/test/java/javalin/examples/HelloWorld.java
new file mode 100644
index 000000000..6d0e3ce62
--- /dev/null
+++ b/src/test/java/javalin/examples/HelloWorld.java
@@ -0,0 +1,10 @@
+package javalin.examples;
+
+import javalin.Javalin;
+
+public class HelloWorld {
+ public static void main(String[] args) {
+ Javalin app = Javalin.create().port(7000);
+ app.get("/", (req, res) -> res.body("Hello World"));
+ }
+}
diff --git a/src/test/java/javalin/examples/HelloWorldApi.java b/src/test/java/javalin/examples/HelloWorldApi.java
new file mode 100644
index 000000000..6f8d24f24
--- /dev/null
+++ b/src/test/java/javalin/examples/HelloWorldApi.java
@@ -0,0 +1,23 @@
+package javalin.examples;
+
+import javalin.Javalin;
+
+import static javalin.ApiBuilder.*;
+
+public class HelloWorldApi {
+
+ public static void main(String[] args) {
+ Javalin.create()
+ .port(7070)
+ .routes(() -> {
+ get("/hello", (req, res) -> res.body("Hello World"));
+ path("/api", () -> {
+ get("/test", (req, res) -> res.body("Hello World"));
+ get("/tast", (req, res) -> res.status(200).body("Hello world"));
+ get("/hest", (req, res) -> res.status(200).body("Hello World"));
+ get("/hast", (req, res) -> res.status(200).body("Hello World").header("test", "tast"));
+ });
+ });
+ }
+
+}
diff --git a/src/test/java/javalin/examples/HelloWorldAuth.java b/src/test/java/javalin/examples/HelloWorldAuth.java
new file mode 100644
index 000000000..cb098406e
--- /dev/null
+++ b/src/test/java/javalin/examples/HelloWorldAuth.java
@@ -0,0 +1,38 @@
+package javalin.examples;
+
+import javalin.Javalin;
+import javalin.security.Role;
+
+import static javalin.ApiBuilder.*;
+import static javalin.examples.HelloWorldAuth.MyRoles.*;
+import static javalin.security.Role.roles;
+
+public class HelloWorldAuth {
+
+ enum MyRoles implements Role {
+ ROLE_ONE, ROLE_TWO, ROLE_THREE;
+ }
+
+ public static void main(String[] args) {
+ Javalin.create()
+ .port(7070)
+ .accessManager((handler, request, response, permittedRoles) -> {
+ String userRole = request.queryParam("role");
+ if (userRole != null && permittedRoles.contains(MyRoles.valueOf(userRole))) {
+ handler.handle(request, response);
+ } else {
+ response.status(401).body("Unauthorized");
+ }
+ })
+ .routes(() -> {
+ get("/hello", (req, res) -> res.body("Hello World 1"), roles(ROLE_ONE));
+ path("/api", () -> {
+ get("/test", (req, res) -> res.body("Hello World 2"), roles(ROLE_TWO));
+ get("/tast", (req, res) -> res.status(200).body("Hello world 3"), roles(ROLE_THREE));
+ get("/hest", (req, res) -> res.status(200).body("Hello World 4"), roles(ROLE_ONE, ROLE_TWO));
+ get("/hast", (req, res) -> res.status(200).body("Hello World 5").header("test", "tast"), roles(ROLE_ONE, ROLE_THREE));
+ });
+ });
+ }
+
+}
diff --git a/src/test/java/javalin/examples/HelloWorldSecure.java b/src/test/java/javalin/examples/HelloWorldSecure.java
new file mode 100644
index 000000000..46dbd9c86
--- /dev/null
+++ b/src/test/java/javalin/examples/HelloWorldSecure.java
@@ -0,0 +1,35 @@
+package javalin.examples;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+
+import javalin.Javalin;
+import javalin.embeddedserver.EmbeddedServer;
+import javalin.embeddedserver.jetty.EmbeddedJettyFactory;
+
+public class HelloWorldSecure {
+
+ public static void main(String[] args) {
+ Javalin.create()
+ .embeddedServer(new EmbeddedJettyFactory(() -> {
+ Server server = new Server();
+ ServerConnector sslConnector = new ServerConnector(server, getSslContextFactory());
+ sslConnector.setPort(443);
+ ServerConnector connector = new ServerConnector(server);
+ connector.setPort(80);
+ server.setConnectors(new Connector[] {sslConnector, connector});
+ return server;
+ }))
+ .get("/", (req, res) -> res.body("Hello World")); // valid endpoint for both connectors
+ }
+
+ private static SslContextFactory getSslContextFactory() {
+ SslContextFactory sslContextFactory = new SslContextFactory();
+ sslContextFactory.setKeyStorePath(EmbeddedServer.class.getResource("/keystore.jks").toExternalForm());
+ sslContextFactory.setKeyStorePassword("password");
+ return sslContextFactory;
+ }
+
+}
diff --git a/src/test/java/javalin/examples/HelloWorldStaticFiles.java b/src/test/java/javalin/examples/HelloWorldStaticFiles.java
new file mode 100644
index 000000000..92d912f51
--- /dev/null
+++ b/src/test/java/javalin/examples/HelloWorldStaticFiles.java
@@ -0,0 +1,13 @@
+package javalin.examples;
+
+import javalin.Javalin;
+
+public class HelloWorldStaticFiles {
+
+ public static void main(String[] args) {
+ Javalin.create()
+ .port(7070)
+ .enableStaticFiles("/public");
+ }
+
+}
diff --git a/src/test/java/javalin/performance/StupidPerformanceTest.java b/src/test/java/javalin/performance/StupidPerformanceTest.java
new file mode 100644
index 000000000..f3c09ebdd
--- /dev/null
+++ b/src/test/java/javalin/performance/StupidPerformanceTest.java
@@ -0,0 +1,75 @@
+package javalin.performance;
+
+import java.io.IOException;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import javalin.Handler;
+import javalin.Javalin;
+
+import com.mashape.unirest.http.HttpResponse;
+import com.mashape.unirest.http.Unirest;
+
+import static javalin.ApiBuilder.*;
+
+public class StupidPerformanceTest {
+
+ private static Javalin app;
+
+ @AfterClass
+ public static void tearDown() {
+ app.stop();
+ }
+
+ @BeforeClass
+ public static void setup() throws IOException {
+ app = Javalin.create()
+ .port(7000)
+ .routes(() -> {
+ before((req, res) -> res.status(123));
+ before((req, res) -> res.status(200));
+ get("/hello", simpleAnswer("Hello from level 0"));
+ path("/level-1", () -> {
+ get("/hello", simpleAnswer("Hello from level 1"));
+ get("/hello-2", simpleAnswer("Hello again from level 1"));
+ get("/param/:param", (req, res) -> {
+ res.body(req.param("param"));
+ });
+ get("/queryparam", (req, res) -> {
+ res.body(req.queryParam("queryparam"));
+ });
+ post("/create-1", simpleAnswer("Created something at level 1"));
+ path("/level-2", () -> {
+ get("/hello", simpleAnswer("Hello from level 2"));
+ path("/level-3", () -> {
+ get("/hello", simpleAnswer("Hello from level 3"));
+ });
+ });
+ });
+ after((req, res) -> res.header("X-AFTER", "After"));
+ });
+ }
+
+ private static Handler simpleAnswer(String body) {
+ return (req, res) -> res.body(body);
+ }
+
+ @Test
+ @Ignore
+ public void testPerformanceMaybe() throws Exception {
+
+ long startTime = System.currentTimeMillis();
+ HttpResponse response;
+ for (int i = 0; i < 1000; i++) {
+ response = Unirest.get("http://localhost:7000/param/test").asString();
+ response = Unirest.get("http://localhost:7000/queryparam/").queryString("queryparam", "value").asString();
+ response = Unirest.get("http://localhost:7000/level-1/level-2/level-3/hello").asString();
+ response = Unirest.get("http://localhost:7000/level-1/level-2/level-3/hello").asString();
+ }
+ System.out.println("took " + (System.currentTimeMillis() - startTime) + " milliseconds");
+ }
+
+}
diff --git a/src/test/java/javalin/util/SimpleHttpClient.java b/src/test/java/javalin/util/SimpleHttpClient.java
new file mode 100644
index 000000000..1ec3a478e
--- /dev/null
+++ b/src/test/java/javalin/util/SimpleHttpClient.java
@@ -0,0 +1,53 @@
+package javalin.util;
+
+import java.io.IOException;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
+import org.apache.http.util.EntityUtils;
+
+public class SimpleHttpClient {
+
+ private HttpClient httpClient;
+
+ public SimpleHttpClient() {
+ this.httpClient = httpClientBuilder().build();
+ }
+
+ private HttpClientBuilder httpClientBuilder() {
+ return HttpClientBuilder.create().setConnectionManager(
+ new BasicHttpClientConnectionManager(
+ RegistryBuilder.create()
+ .register("http", PlainConnectionSocketFactory.INSTANCE)
+ .build()
+ )
+ );
+ }
+
+ public TestResponse http_GET(String path) throws IOException {
+ HttpResponse httpResponse = httpClient.execute(new HttpGet(path));
+ HttpEntity entity = httpResponse.getEntity();
+ return new TestResponse(
+ EntityUtils.toString(entity),
+ httpResponse.getStatusLine().getStatusCode()
+ );
+ }
+
+ public static class TestResponse {
+ public String body;
+ public int status;
+
+ private TestResponse(String body, int status) {
+ this.body = body;
+ this.status = status;
+ }
+ }
+
+}
diff --git a/src/test/java/javalin/util/TestObject_NonSerializable.java b/src/test/java/javalin/util/TestObject_NonSerializable.java
new file mode 100644
index 000000000..89f910667
--- /dev/null
+++ b/src/test/java/javalin/util/TestObject_NonSerializable.java
@@ -0,0 +1,11 @@
+package javalin.util;
+
+public class TestObject_NonSerializable {
+
+ private String value1 = "First value";
+ private String value2 = "Second value";
+
+ public TestObject_NonSerializable() {
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/javalin/util/TestObject_Serializable.java b/src/test/java/javalin/util/TestObject_Serializable.java
new file mode 100644
index 000000000..b2af00615
--- /dev/null
+++ b/src/test/java/javalin/util/TestObject_Serializable.java
@@ -0,0 +1,11 @@
+package javalin.util;
+
+public class TestObject_Serializable {
+
+ public String value1 = "First value";
+ public String value2 = "Second value";
+
+ public TestObject_Serializable() {
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/javalin/util/TypedException.java b/src/test/java/javalin/util/TypedException.java
new file mode 100644
index 000000000..cfd44941a
--- /dev/null
+++ b/src/test/java/javalin/util/TypedException.java
@@ -0,0 +1,7 @@
+package javalin.util;
+
+public class TypedException extends Exception {
+ public String proofOfType() {
+ return "I'm so typed";
+ }
+}
diff --git a/src/test/resources/keystore.jks b/src/test/resources/keystore.jks
new file mode 100644
index 000000000..cc61ad46a
Binary files /dev/null and b/src/test/resources/keystore.jks differ
diff --git a/src/test/resources/public/html.html b/src/test/resources/public/html.html
new file mode 100644
index 000000000..dc0197f84
--- /dev/null
+++ b/src/test/resources/public/html.html
@@ -0,0 +1,10 @@
+
+
+
+
+
+ HTML works
+
+
+
+
diff --git a/src/test/resources/public/protected/secret.html b/src/test/resources/public/protected/secret.html
new file mode 100644
index 000000000..e091323d5
--- /dev/null
+++ b/src/test/resources/public/protected/secret.html
@@ -0,0 +1 @@
+Secret file
\ No newline at end of file
diff --git a/src/test/resources/public/script.js b/src/test/resources/public/script.js
new file mode 100644
index 000000000..cf3d7df3a
--- /dev/null
+++ b/src/test/resources/public/script.js
@@ -0,0 +1 @@
+document.write("JavaScript works ");
diff --git a/src/test/resources/public/styles.css b/src/test/resources/public/styles.css
new file mode 100644
index 000000000..9c0e6d043
--- /dev/null
+++ b/src/test/resources/public/styles.css
@@ -0,0 +1,3 @@
+.css-test:before {
+ content: "CSS works"
+}
diff --git a/src/test/resources/public/subdir/index.html b/src/test/resources/public/subdir/index.html
new file mode 100644
index 000000000..a2096bea3
--- /dev/null
+++ b/src/test/resources/public/subdir/index.html
@@ -0,0 +1 @@
+Welcome file
\ No newline at end of file