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 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 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 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