diff --git a/avaje-jex-grizzly-spi/pom.xml b/avaje-jex-grizzly-spi/pom.xml new file mode 100644 index 00000000..e7c61d20 --- /dev/null +++ b/avaje-jex-grizzly-spi/pom.xml @@ -0,0 +1,39 @@ + + 4.0.0 + + io.avaje + avaje-jex-parent + 3.0-RC23 + + 0.1 + avaje-jex-grizzly-spi + + + + + io.avaje + avaje-jex + + + + org.glassfish.grizzly + grizzly-http-server + 4.1.0-M1 + + + io.avaje + avaje-spi-service + provided + true + + + + io.avaje + avaje-jex-test + test + + + + diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyExchange.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyExchange.java new file mode 100644 index 00000000..10c7fb4f --- /dev/null +++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyExchange.java @@ -0,0 +1,10 @@ +package io.avaje.jex.grizzly.spi; + +import com.sun.net.httpserver.HttpPrincipal; + +sealed interface GrizzlyExchange permits GrizzlyHttpExchange, GrizzlyHttpsExchange { + + HttpPrincipal getPrincipal(); + + void setPrincipal(HttpPrincipal principal); +} diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHandler.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHandler.java new file mode 100644 index 00000000..4ea79c85 --- /dev/null +++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHandler.java @@ -0,0 +1,48 @@ +package io.avaje.jex.grizzly.spi; + +import java.io.IOException; +import java.io.UncheckedIOException; + +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.grizzly.http.server.Response; + +import com.sun.net.httpserver.Filter.Chain; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +final class GrizzlyHandler extends org.glassfish.grizzly.http.server.HttpHandler { + + private final HttpContext httpContext; + + private HttpHandler handler; + + GrizzlyHandler(HttpContext httpContext, HttpHandler httpHandler) { + super(httpContext.getPath()); + this.httpContext = httpContext; + this.handler = httpHandler; + } + + @Override + public void service(Request request, Response response) { + + try (HttpExchange exchange = + request.isSecure() + ? new GrizzlyHttpsExchange(httpContext, request, response) + : new GrizzlyHttpExchange(httpContext, request, response)) { + + new Chain(httpContext.getFilters(), handler).doFilter(exchange); + + } catch (IOException ex) { + throw new UncheckedIOException(null); + } + } + + public HttpHandler getHttpHandler() { + return handler; + } + + public void setHttpHandler(HttpHandler handler) { + this.handler = handler; + } +} diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpContext.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpContext.java new file mode 100644 index 00000000..5ede6a56 --- /dev/null +++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpContext.java @@ -0,0 +1,77 @@ +package io.avaje.jex.grizzly.spi; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.sun.net.httpserver.Authenticator; +import com.sun.net.httpserver.Filter; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +final class GrizzlyHttpContext extends com.sun.net.httpserver.HttpContext { + + private final GrizzlyHandler grizzlyHandler; + private final HttpServer server; + + private final Map attributes = new HashMap<>(); + + private final List filters = new ArrayList<>(); + + private Authenticator authenticator; + + private String contextPath; + + protected GrizzlyHttpContext(HttpServer server, String contextPath, HttpHandler handler) { + this.server = server; + this.grizzlyHandler = new GrizzlyHandler(this, handler); + this.contextPath = contextPath; + } + + GrizzlyHandler getGrizzlyHandler() { + return grizzlyHandler; + } + + @Override + public HttpHandler getHandler() { + return grizzlyHandler.getHttpHandler(); + } + + @Override + public void setHandler(HttpHandler h) { + grizzlyHandler.setHttpHandler(h); + } + + @Override + public String getPath() { + return contextPath; + } + + @Override + public HttpServer getServer() { + return server; + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public List getFilters() { + return filters; + } + + @Override + public Authenticator setAuthenticator(Authenticator auth) { + Authenticator previous = authenticator; + authenticator = auth; + return previous; + } + + @Override + public Authenticator getAuthenticator() { + return authenticator; + } +} diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchange.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchange.java new file mode 100644 index 00000000..38fc59c8 --- /dev/null +++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchange.java @@ -0,0 +1,129 @@ +package io.avaje.jex.grizzly.spi; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; + +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.grizzly.http.server.Response; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpPrincipal; + +final class GrizzlyHttpExchange extends HttpExchange implements GrizzlyExchange { + private final GrizzlyHttpExchangeDelegate delegate; + + public GrizzlyHttpExchange(HttpContext context, Request req, Response resp) { + + delegate = new GrizzlyHttpExchangeDelegate(context, req, resp); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public Headers getRequestHeaders() { + return delegate.getRequestHeaders(); + } + + @Override + public Headers getResponseHeaders() { + return delegate.getResponseHeaders(); + } + + @Override + public URI getRequestURI() { + return delegate.getRequestURI(); + } + + @Override + public String getRequestMethod() { + return delegate.getRequestMethod(); + } + + @Override + public HttpContext getHttpContext() { + return delegate.getHttpContext(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public boolean equals(Object obj) { + return delegate.equals(obj); + } + + @Override + public InputStream getRequestBody() { + return delegate.getRequestBody(); + } + + @Override + public OutputStream getResponseBody() { + return delegate.getResponseBody(); + } + + @Override + public void sendResponseHeaders(int rCode, long responseLength) throws IOException { + delegate.sendResponseHeaders(rCode, responseLength); + } + + @Override + public InetSocketAddress getRemoteAddress() { + return delegate.getRemoteAddress(); + } + + @Override + public int getResponseCode() { + return delegate.getResponseCode(); + } + + @Override + public InetSocketAddress getLocalAddress() { + return delegate.getLocalAddress(); + } + + @Override + public String getProtocol() { + return delegate.getProtocol(); + } + + @Override + public Object getAttribute(String name) { + return delegate.getAttribute(name); + } + + @Override + public void setAttribute(String name, Object value) { + delegate.setAttribute(name, value); + } + + @Override + public void setStreams(InputStream i, OutputStream o) { + delegate.setStreams(i, o); + } + + @Override + public HttpPrincipal getPrincipal() { + return delegate.getPrincipal(); + } + + @Override + public void setPrincipal(HttpPrincipal principal) { + delegate.setPrincipal(principal); + } + + @Override + public String toString() { + return delegate.toString(); + } +} diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchangeDelegate.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchangeDelegate.java new file mode 100644 index 00000000..a6b6982c --- /dev/null +++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpExchangeDelegate.java @@ -0,0 +1,193 @@ +package io.avaje.jex.grizzly.spi; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.grizzly.http.server.Response; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpPrincipal; + +final class GrizzlyHttpExchangeDelegate extends HttpExchange { + + /** Set of headers that RFC9110 says will not have a value list */ + private static final Set SINGLE_VALUE_HEADERS = + Set.of( + "authorization", + "content-length", + "date", + "expires", + "host", + "if-modified-since", + "if-unmodified-since", + "if-range", + "last-modified", + "location", + "referer", + "retry-after", + "user-agent"); + + private final HttpContext context; + + private final Request request; + + private final Headers responseHeaders = new Headers(); + + private Headers requestHeaders = new Headers(); + + private int statusCode = 0; + + private InputStream inputStream; + + private OutputStream outputStream; + + private HttpPrincipal httpPrincipal; + + private Response response; + + GrizzlyHttpExchangeDelegate(HttpContext httpSpiContext, Request request, Response response) { + this.context = httpSpiContext; + this.request = request; + this.response = response; + this.inputStream = request.getInputStream(); + this.outputStream = response.getOutputStream(); + } + + @Override + public Headers getRequestHeaders() { + + if (!requestHeaders.isEmpty()) { + return requestHeaders; + } + for (var name : request.getHeaderNames()) { + + if (!SINGLE_VALUE_HEADERS.contains(name.toLowerCase())) { + + for (String value : request.getHeaders(name)) { + requestHeaders.add(name, value); + } + } else { + requestHeaders.add(name, request.getHeader(name)); + } + } + return requestHeaders; + } + + @Override + public Headers getResponseHeaders() { + return responseHeaders; + } + + @Override + public URI getRequestURI() { + return URI.create(request.getRequestURI()); + } + + @Override + public String getRequestMethod() { + return request.getMethod().getMethodString(); + } + + @Override + public HttpContext getHttpContext() { + return context; + } + + @Override + public void close() { + try { + outputStream.close(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + @Override + public InputStream getRequestBody() { + return inputStream; + } + + @Override + public OutputStream getResponseBody() { + return outputStream; + } + + @Override + public void sendResponseHeaders(int rCode, long responseLength) throws IOException { + this.statusCode = rCode; + + for (Map.Entry> stringListEntry : responseHeaders.entrySet()) { + String name = stringListEntry.getKey(); + List values = stringListEntry.getValue(); + for (String value : values) { + response.addHeader(name, value); + } + } + + if (responseLength == -1) { + response.setContentLengthLong(0); + } else if (responseLength == 0) { + response.setContentLengthLong(-1); + } else { + response.setContentLengthLong(responseLength); + } + + response.setStatus(rCode); + } + + @Override + public InetSocketAddress getRemoteAddress() { + return InetSocketAddress.createUnresolved(request.getRemoteAddr(), request.getRemotePort()); + } + + @Override + public int getResponseCode() { + return statusCode; + } + + @Override + public InetSocketAddress getLocalAddress() { + return new InetSocketAddress(request.getLocalAddr(), request.getLocalPort()); + } + + @Override + public String getProtocol() { + return request.getProtocol().getProtocolString(); + } + + @Override + public Object getAttribute(String name) { + return request.getAttribute(name); + } + + @Override + public void setAttribute(String name, Object value) { + request.setAttribute(name, value); + } + + @Override + public void setStreams(InputStream i, OutputStream o) { + assert inputStream != null; + if (i != null) inputStream = i; + if (o != null) outputStream = o; + } + + @Override + public HttpPrincipal getPrincipal() { + return httpPrincipal; + } + + public void setPrincipal(HttpPrincipal principal) { + this.httpPrincipal = principal; + } +} diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServer.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServer.java new file mode 100644 index 00000000..cdd9f79c --- /dev/null +++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServer.java @@ -0,0 +1,161 @@ +package io.avaje.jex.grizzly.spi; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.System.Logger.Level; +import java.net.InetSocketAddress; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.glassfish.grizzly.http.server.HttpServer; +import org.glassfish.grizzly.http.server.NetworkListener; +import org.glassfish.grizzly.http.server.ServerConfiguration; +import org.glassfish.grizzly.ssl.SSLEngineConfigurator; + +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpsConfigurator; + +final class GrizzlyHttpServer extends com.sun.net.httpserver.HttpsServer { + private static final System.Logger LOG = + System.getLogger(GrizzlyHttpServer.class.getCanonicalName()); + private final HttpServer server; + private InetSocketAddress addr; + private ServerConfiguration httpConfiguration; + private ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + private HttpsConfigurator httpsConfig; + + public GrizzlyHttpServer(HttpServer server) { + + this(server, server.getServerConfiguration()); + } + + public GrizzlyHttpServer(HttpServer server, ServerConfiguration configuration) { + this.server = server; + this.httpConfiguration = configuration; + } + + public ServerConfiguration getHttpConfiguration() { + return httpConfiguration; + } + + @Override + public void bind(InetSocketAddress addr, int backlog) throws IOException { + + this.addr = addr; + // check if there is already a connector listening + var connectors = server.getListeners(); + if (connectors != null) { + for (var connector : connectors) { + if (connector.getPort() == addr.getPort()) { + LOG.log( + Level.DEBUG, "server already bound to port {}, no need to rebind", addr.getPort()); + return; + } + } + } + + if (LOG.isLoggable(Level.DEBUG)) { + LOG.log(Level.DEBUG, "binding server to port " + addr.getPort()); + } + var listener = new NetworkListener("rizzly", addr.getHostName(), addr.getPort()); + listener.getTransport().setWorkerThreadPool(executor); + if (backlog != 0) { + listener.getTransport().setServerConnectionBackLog(backlog); + } + if (httpsConfig != null) { + listener.setSSLEngineConfig(new SSLEngineConfigurator(httpsConfig.getSSLContext())); + } + + server.addListener(listener); + } + + protected HttpServer getServer() { + return server; + } + + @Override + public InetSocketAddress getAddress() { + if (addr.getPort() == 0 && server.isStarted()) { + return new InetSocketAddress(addr.getHostString(), server.getListener("rizzly").getPort()); + } + return addr; + } + + @Override + public void start() { + + try { + server.start(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void setExecutor(Executor executor) { + if (executor instanceof ExecutorService service) { + this.executor = service; + } else { + throw new IllegalArgumentException("Grizzly only accepts an instance of ExecutorService"); + } + } + + @Override + public Executor getExecutor() { + return executor; + } + + @Override + public void stop(int delay) { + server.shutdownNow(); + } + + @Override + public HttpContext createContext(String path, HttpHandler httpHandler) { + + GrizzlyHttpContext context = new GrizzlyHttpContext(this, path, httpHandler); + GrizzlyHandler jettyContextHandler = context.getGrizzlyHandler(); + + httpConfiguration.addHttpHandler( + jettyContextHandler, path.transform(this::prependSlash).transform(this::appendSlash)); + + return context; + } + + private String prependSlash(String s) { + return s.startsWith("/") ? s : "/" + s; + } + + private String appendSlash(String s) { + return s.endsWith("/") ? s + "*" : s + "/*"; + } + + @Override + public HttpContext createContext(String path) { + return createContext(path, null); + } + + @Override + public void removeContext(String path) { + + throw new UnsupportedOperationException("notImplemented"); + } + + @Override + public void removeContext(HttpContext context) { + + throw new UnsupportedOperationException("notImplemented"); + } + + @Override + public void setHttpsConfigurator(HttpsConfigurator config) { + httpsConfig = config; + } + + @Override + public HttpsConfigurator getHttpsConfigurator() { + return httpsConfig; + } +} diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServerProvider.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServerProvider.java new file mode 100644 index 00000000..bd21367d --- /dev/null +++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpServerProvider.java @@ -0,0 +1,50 @@ +package io.avaje.jex.grizzly.spi; + +import java.io.IOException; +import java.net.InetSocketAddress; + +import org.glassfish.grizzly.http.server.HttpServer; + +import com.sun.net.httpserver.HttpsServer; +import com.sun.net.httpserver.spi.HttpServerProvider; + +import io.avaje.spi.ServiceProvider; + +@ServiceProvider +public class GrizzlyHttpServerProvider extends HttpServerProvider { + + private org.glassfish.grizzly.http.server.HttpServer server; + + public GrizzlyHttpServerProvider(HttpServer server) { + + this.server = server; + } + + public GrizzlyHttpServerProvider() { + + this.server = new HttpServer(); + } + + @Override + public com.sun.net.httpserver.HttpServer createHttpServer(InetSocketAddress addr, int backlog) + throws IOException { + + return createServer(addr, backlog); + } + + @Override + public HttpsServer createHttpsServer(InetSocketAddress addr, int backlog) throws IOException { + return createServer(addr, backlog); + } + + private com.sun.net.httpserver.HttpsServer createServer(InetSocketAddress addr, int backlog) + throws IOException { + if (server == null) { + server = new HttpServer(); + } + + GrizzlyHttpServer jettyHttpServer = new GrizzlyHttpServer(server); + if (addr != null) jettyHttpServer.bind(addr, backlog); + return jettyHttpServer; + } +} diff --git a/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpsExchange.java b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpsExchange.java new file mode 100644 index 00000000..d1e686d6 --- /dev/null +++ b/avaje-jex-grizzly-spi/src/main/java/io/avaje/jex/grizzly/spi/GrizzlyHttpsExchange.java @@ -0,0 +1,125 @@ +package io.avaje.jex.grizzly.spi; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; + +import javax.net.ssl.SSLSession; + +import org.glassfish.grizzly.http.server.Request; +import org.glassfish.grizzly.http.server.Response; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpContext; +import com.sun.net.httpserver.HttpPrincipal; +import com.sun.net.httpserver.HttpsExchange; + +final class GrizzlyHttpsExchange extends HttpsExchange implements GrizzlyExchange { + private final GrizzlyHttpExchangeDelegate delegate; + + public GrizzlyHttpsExchange(HttpContext jaxWsContext, Request req, Response resp) { + delegate = new GrizzlyHttpExchangeDelegate(jaxWsContext, req, resp); + } + + @Override + public Headers getRequestHeaders() { + return delegate.getRequestHeaders(); + } + + @Override + public Headers getResponseHeaders() { + return delegate.getResponseHeaders(); + } + + @Override + public URI getRequestURI() { + return delegate.getRequestURI(); + } + + @Override + public String getRequestMethod() { + return delegate.getRequestMethod(); + } + + @Override + public HttpContext getHttpContext() { + return delegate.getHttpContext(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public InputStream getRequestBody() { + return delegate.getRequestBody(); + } + + @Override + public OutputStream getResponseBody() { + return delegate.getResponseBody(); + } + + @Override + public void sendResponseHeaders(int rCode, long responseLength) throws IOException { + delegate.sendResponseHeaders(rCode, responseLength); + } + + @Override + public InetSocketAddress getRemoteAddress() { + return delegate.getRemoteAddress(); + } + + @Override + public int getResponseCode() { + return delegate.getResponseCode(); + } + + @Override + public InetSocketAddress getLocalAddress() { + return delegate.getLocalAddress(); + } + + @Override + public String getProtocol() { + return delegate.getProtocol(); + } + + @Override + public Object getAttribute(String name) { + return delegate.getAttribute(name); + } + + @Override + public void setAttribute(String name, Object value) { + delegate.setAttribute(name, value); + } + + @Override + public void setStreams(InputStream i, OutputStream o) { + delegate.setStreams(i, o); + } + + @Override + public HttpPrincipal getPrincipal() { + return delegate.getPrincipal(); + } + + @Override + public void setPrincipal(HttpPrincipal principal) { + delegate.setPrincipal(principal); + } + + @Override + public String toString() { + return delegate.toString(); + } + + @Override + public SSLSession getSSLSession() { + return null; + } +} diff --git a/avaje-jex-grizzly-spi/src/main/java/module-info.java b/avaje-jex-grizzly-spi/src/main/java/module-info.java new file mode 100644 index 00000000..6378b580 --- /dev/null +++ b/avaje-jex-grizzly-spi/src/main/java/module-info.java @@ -0,0 +1,17 @@ +import com.sun.net.httpserver.spi.HttpServerProvider; + +module io.avaje.jex.grizzly { + + exports io.avaje.jex.grizzly.spi; + + requires transitive io.avaje.jex; + requires transitive jdk.httpserver; + requires transitive org.glassfish.grizzly.http.server; + requires transitive org.glassfish.grizzly.http; + requires transitive org.glassfish.grizzly; + + requires static io.avaje.spi; + requires static java.net.http; + + provides HttpServerProvider with io.avaje.jex.grizzly.spi.GrizzlyHttpServerProvider; +} diff --git a/avaje-jex-grizzly-spi/src/test/java/io/avaje/jex/grizzly/spi/FilterTest.java b/avaje-jex-grizzly-spi/src/test/java/io/avaje/jex/grizzly/spi/FilterTest.java new file mode 100644 index 00000000..fc6cc485 --- /dev/null +++ b/avaje-jex-grizzly-spi/src/test/java/io/avaje/jex/grizzly/spi/FilterTest.java @@ -0,0 +1,119 @@ +package io.avaje.jex.grizzly.spi; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.http.HttpHeaders; +import java.net.http.HttpResponse; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.LockSupport; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import io.avaje.jex.Jex; +import io.avaje.jex.test.TestPair; + +class FilterTest { + + static final TestPair pair = init(); + static final AtomicReference afterAll = new AtomicReference<>(); + static final AtomicReference afterTwo = new AtomicReference<>(); + + static TestPair init() { + final Jex app = + Jex.create() + .routing( + routing -> + routing + .get("/", ctx -> ctx.text("roo")) + .get( + "/noResponse", + ctx -> { + ctx.header("Content-Type", ""); + }) + .get("/one", ctx -> ctx.text("one")) + .get("/two", ctx -> ctx.text("two")) + .get("/two/{id}", ctx -> ctx.text("two-id")) + .before(ctx -> ctx.header("before-all", "set")) + .filter( + (ctx, chain) -> { + if (ctx.path().contains("/two/")) { + ctx.header("before-two", "set"); + } + chain.proceed(); + }) + .after(ctx -> afterAll.set("set")) + .filter( + (ctx, chain) -> { + chain.proceed(); + if (ctx.path().contains("/two/")) { + afterTwo.set("set"); + } + }) + .get("/dummy", ctx -> ctx.text("dummy"))); + + return TestPair.create(app); + } + + @AfterAll + static void end() { + pair.shutdown(); + } + + void clearAfter() { + afterAll.set(null); + afterTwo.set(null); + } + + @Test + void get() { + clearAfter(); + HttpResponse res = pair.request().GET().asString(); + assertHasBeforeAfterAll(res); + assertNoBeforeAfterTwo(res); + + clearAfter(); + res = pair.request().path("one").GET().asString(); + assertHasBeforeAfterAll(res); + assertNoBeforeAfterTwo(res); + + clearAfter(); + res = pair.request().path("two").GET().asString(); + assertHasBeforeAfterAll(res); + assertNoBeforeAfterTwo(res); + } + + @Test + void getNoResponse() { + clearAfter(); + HttpResponse res = pair.request().path("noResponse").GET().asString(); + assertThat(res.statusCode()).isEqualTo(204); + assertHasBeforeAfterAll(res); + assertNoBeforeAfterTwo(res); + } + + @Test + void get_two_expect_extraFilters() { + clearAfter(); + HttpResponse res = pair.request().path("two/42").GET().asString(); + + final HttpHeaders headers = res.headers(); + assertHasBeforeAfterAll(res); + assertThat(headers.firstValue("before-two")).get().isEqualTo("set"); + assertThat(afterTwo.get()).isEqualTo("set"); + } + + private void assertNoBeforeAfterTwo(HttpResponse res) { + assertThat(res.statusCode()).isLessThan(300); + assertThat(res.headers().firstValue("before-two")).isEmpty(); + assertThat(afterTwo.get()).isNull(); + } + + private void assertHasBeforeAfterAll(HttpResponse res) { + assertThat(res.statusCode()).isLessThan(300); + assertThat(res.headers().firstValue("before-all")).get().isEqualTo("set"); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(2)); + assertThat(afterAll.get()).isEqualTo("set"); + } +} diff --git a/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java b/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java index cd91a6ef..f224049d 100644 --- a/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java +++ b/avaje-jex/src/main/java/io/avaje/jex/core/RoutingHandler.java @@ -60,7 +60,7 @@ public void handle(HttpExchange exchange) { } private void handleNoResponse(HttpExchange exchange) throws IOException { - if (exchange.getResponseCode() == -1) { + if (exchange.getResponseCode() < 1) { exchange.sendResponseHeaders(204, -1); } } diff --git a/pom.xml b/pom.xml index 4a7f6556..3cb28519 100644 --- a/pom.xml +++ b/pom.xml @@ -39,11 +39,12 @@ avaje-jex - avaje-jex-test avaje-jex-freemarker - avaje-jex-mustache + avaje-jex-grizzly-spi avaje-jex-htmx + avaje-jex-mustache avaje-jex-static-content + avaje-jex-test