enhanced router middleware support; added pre-fused middleware handling and improved handler registration

This commit is contained in:
Relism
2026-03-19 21:35:54 +01:00
parent 16b5f8ac15
commit 9a30c5ab16
10 changed files with 204 additions and 85 deletions
Generated
+1
View File
@@ -2,5 +2,6 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$/nuxt-shadcn-dashboard" vcs="Git" />
</component> </component>
</project> </project>
+27 -27
View File
@@ -4,37 +4,19 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="optimized dynamic body size impl, decluttering javadocs/comments"> <list default="true" id="fc757130-fe3e-4ea9-8d69-c26ffb8545a4" name="Changes" comment="multipart parsing, request body access, and chunked input stream support">
<change afterPath="$PROJECT_DIR$/flash-bench/src/main/java/dev/relism/bench/StaticResourceManager.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash-bench/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-bench/pom.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash-bench/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/flash-bench/pom.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash-bench/src/main/java/dev/relism/bench/Main.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash-bench/src/main/java/dev/relism/bench/Main.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServer.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServer.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServer.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServer.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServerConfiguration.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/HttpServerConfiguration.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Main.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Main.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Main.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/Main.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/RequestParser.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/RequestParser.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/ContentType.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/ContentType.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpStatus.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/http/HttpStatus.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/HeaderMap.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/HeaderMap.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/LazyBody.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/QueryParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/QueryParams.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/QueryParams.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/QueryParams.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Request.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Request.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/RequestHandler.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Response.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Response.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Response.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/Response.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/SimpleHandler.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/models/SimpleHandler.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/AbstractRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/AbstractRouter.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/AbstractRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/AbstractRouter.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathViews.java" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/GlobalRouter.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/GlobalRouter.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/resources/assets/html/default_404.html" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/html/default_404.html" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/Route.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/Route.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/main/resources/assets/html/default_exception.html" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/resources/assets/html/default_exception.html" afterDir="false" /> <change beforePath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/main/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImpl.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerConcurrencyTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerConcurrencyTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/HttpServerTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/RequestParserTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/RequestParserTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/HeaderMapTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/HeaderMapTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestLineTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestLineTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/RequestTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/ResponseTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/models/ResponseTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/GlobalRouterTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/GlobalRouterTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/routing/routers/fastpathrouter/FastPathRouterImplTest.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/flash/src/test/java/dev/relism/template/ErrorPagesTest.java" beforeDir="false" afterPath="$PROJECT_DIR$/flash/src/test/java/dev/relism/template/ErrorPagesTest.java" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -247,7 +229,7 @@
<workItem from="1773837116508" duration="4255000" /> <workItem from="1773837116508" duration="4255000" />
<workItem from="1773847734658" duration="911000" /> <workItem from="1773847734658" duration="911000" />
<workItem from="1773875652058" duration="5481000" /> <workItem from="1773875652058" duration="5481000" />
<workItem from="1773915392332" duration="1223000" /> <workItem from="1773915392332" duration="24154000" />
</task> </task>
<task id="LOCAL-00001" summary="Initial"> <task id="LOCAL-00001" summary="Initial">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -281,7 +263,23 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1773591259108</updated> <updated>1773591259108</updated>
</task> </task>
<option name="localTasksCounter" value="5" /> <task id="LOCAL-00005" summary="enhanced HTTP server configuration and response handling; added acceptorThreads, improved header management, and refined error page titles">
<option name="closed" value="true" />
<created>1773920675327</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1773920675327</updated>
</task>
<task id="LOCAL-00006" summary="multipart parsing, request body access, and chunked input stream support">
<option name="closed" value="true" />
<created>1773920735659</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1773920735659</updated>
</task>
<option name="localTasksCounter" value="7" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -303,7 +301,9 @@
<MESSAGE value="pre-module refactor" /> <MESSAGE value="pre-module refactor" />
<MESSAGE value="refactored, pre-buffer reuse" /> <MESSAGE value="refactored, pre-buffer reuse" />
<MESSAGE value="optimized dynamic body size impl, decluttering javadocs/comments" /> <MESSAGE value="optimized dynamic body size impl, decluttering javadocs/comments" />
<option name="LAST_COMMIT_MESSAGE" value="optimized dynamic body size impl, decluttering javadocs/comments" /> <MESSAGE value="enhanced HTTP server configuration and response handling; added acceptorThreads, improved header management, and refined error page titles" />
<MESSAGE value="multipart parsing, request body access, and chunked input stream support" />
<option name="LAST_COMMIT_MESSAGE" value="multipart parsing, request body access, and chunked input stream support" />
</component> </component>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />
+8 -3
View File
@@ -31,9 +31,14 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.asynchttpclient</groupId> <groupId>org.hibernate.orm</groupId>
<artifactId>async-http-client</artifactId> <artifactId>hibernate-core</artifactId>
<version>${ahc.version}</version> <version>6.6.4.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
</dependency> </dependency>
</dependencies> </dependencies>
+19 -16
View File
@@ -6,6 +6,7 @@ import dev.relism.http.HttpStatus;
import dev.relism.models.*; import dev.relism.models.*;
import dev.relism.routing.AbstractRouter; import dev.relism.routing.AbstractRouter;
import dev.relism.routing.GlobalRouter; import dev.relism.routing.GlobalRouter;
import dev.relism.routing.Middleware;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -29,7 +30,7 @@ import java.util.concurrent.TimeUnit;
public class HttpServer { public class HttpServer {
private final HttpServerConfiguration configuration; private final HttpServerConfiguration configuration;
private final ServerSocket serverSocket; private final ServerSocket serverSocket;
private final GlobalRouter globalRouter = new GlobalRouter(); private final GlobalRouter globalRouter;
private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor(); private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
private final Set<Socket> activeSockets = ConcurrentHashMap.newKeySet(); private final Set<Socket> activeSockets = ConcurrentHashMap.newKeySet();
private volatile boolean stopped = false; private volatile boolean stopped = false;
@@ -52,9 +53,14 @@ public class HttpServer {
DIGITS[i] = String.valueOf(i).getBytes(StandardCharsets.UTF_8); DIGITS[i] = String.valueOf(i).getBytes(StandardCharsets.UTF_8);
} }
public HttpServer(HttpServerConfiguration configuration) throws IOException { /**
* Creates the server with optional server-level middlewares applied to every registered route.
* Middlewares are pre-fused at construction time; the first element executes outermost.
*/
public HttpServer(HttpServerConfiguration configuration, Middleware... middlewares) throws IOException {
this.configuration = configuration; this.configuration = configuration;
this.serverSocket = new ServerSocket(configuration.getPort()); this.serverSocket = new ServerSocket(configuration.getPort());
this.globalRouter = new GlobalRouter(middlewares);
} }
/** Returns a future that completes once the accept loop is running and the server is ready. */ /** Returns a future that completes once the accept loop is running and the server is ready. */
@@ -119,21 +125,18 @@ public class HttpServer {
return this; return this;
} }
public HttpServer register(RequestHandler handler) { public HttpServer register(RequestHandler handler, Middleware... m) { globalRouter.register(handler, m); return this; }
globalRouter.register(handler);
return this;
}
public HttpServer get(String path, SimpleHandler.FunctionalHandler h) { globalRouter.get(path, h); return this; } public HttpServer get (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.get (path, h, m); return this; }
public HttpServer post(String path, SimpleHandler.FunctionalHandler h) { globalRouter.post(path, h); return this; } public HttpServer post (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.post (path, h, m); return this; }
public HttpServer put(String path, SimpleHandler.FunctionalHandler h) { globalRouter.put(path, h); return this; } public HttpServer put (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.put (path, h, m); return this; }
public HttpServer delete(String path, SimpleHandler.FunctionalHandler h) { globalRouter.delete(path, h); return this; } public HttpServer delete (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.delete (path, h, m); return this; }
public HttpServer patch(String path, SimpleHandler.FunctionalHandler h) { globalRouter.patch(path, h); return this; } public HttpServer patch (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.patch (path, h, m); return this; }
public HttpServer options(String path, SimpleHandler.FunctionalHandler h) { globalRouter.options(path, h); return this; } public HttpServer options(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.options(path, h, m); return this; }
public HttpServer head(String path, SimpleHandler.FunctionalHandler h) { globalRouter.head(path, h); return this; } public HttpServer head (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.head (path, h, m); return this; }
public HttpServer trace(String path, SimpleHandler.FunctionalHandler h) { globalRouter.trace(path, h); return this; } public HttpServer trace (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.trace (path, h, m); return this; }
public HttpServer connect(String path, SimpleHandler.FunctionalHandler h) { globalRouter.connect(path, h); return this; } public HttpServer connect(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.connect(path, h, m); return this; }
public HttpServer purge(String path, SimpleHandler.FunctionalHandler h) { globalRouter.purge(path, h); return this; } public HttpServer purge (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { globalRouter.purge (path, h, m); return this; }
private void process(Socket socket) { private void process(Socket socket) {
@@ -8,7 +8,10 @@ import java.util.List;
/** /**
* Lazy query parameter access ({@code ?key=value&...}). Backed by a zero-copy {@link ByteView} * Lazy query parameter access ({@code ?key=value&...}). Backed by a zero-copy {@link ByteView}
* over the raw query string bytes, no parsing at construction, values decoded on demand. * over the raw query string bytes, no parsing at construction, values percent-decoded on demand.
*
* <p>Percent-decoding ({@code %XX} → byte, {@code +} → space) follows the
* {@code application/x-www-form-urlencoded} convention used by all browsers.
*/ */
public class QueryParams { public class QueryParams {
public static final QueryParams EMPTY = new QueryParams(null); public static final QueryParams EMPTY = new QueryParams(null);
@@ -22,10 +25,7 @@ public class QueryParams {
public String get(String name) { public String get(String name) {
long r = findFirst(name); long r = findFirst(name);
if (r < 0) return null; if (r < 0) return null;
int s = (int) (r >> 32), l = (int) (r & 0xFFFFFFFFL); return decode((int) (r >> 32), (int) (r & 0xFFFFFFFFL));
byte[] bytes = new byte[l];
for (int i = 0; i < l; i++) bytes[i] = raw.byteAt(s + i);
return new String(bytes, StandardCharsets.UTF_8);
} }
ByteView view(String name) { ByteView view(String name) {
@@ -51,11 +51,8 @@ public class QueryParams {
int valStart = i; int valStart = i;
while (i < len && raw.byteAt(i) != '&') i++; while (i < len && raw.byteAt(i) != '&') i++;
if (keyMatches(keyStart, keyLen, name)) { if (keyMatches(keyStart, keyLen, name)) {
int valLen = i - valStart;
byte[] bytes = new byte[valLen];
for (int j = 0; j < valLen; j++) bytes[j] = raw.byteAt(valStart + j);
if (result == null) result = new ArrayList<>(); if (result == null) result = new ArrayList<>();
result.add(new String(bytes, StandardCharsets.UTF_8)); result.add(decode(valStart, i - valStart));
} }
} }
if (i < len && raw.byteAt(i) == '&') i++; if (i < len && raw.byteAt(i) == '&') i++;
@@ -63,6 +60,8 @@ public class QueryParams {
return result != null ? result : List.of(); return result != null ? result : List.of();
} }
// ── Internals ─────────────────────────────────────────────────────────────
/** Returns (valStart << 32) | valLen, or -1 if not found. */ /** Returns (valStart << 32) | valLen, or -1 if not found. */
private long findFirst(String name) { private long findFirst(String name) {
if (raw == null) return -1L; if (raw == null) return -1L;
@@ -87,4 +86,39 @@ public class QueryParams {
for (int i = 0; i < len; i++) if (raw.byteAt(start + i) != (byte) name.charAt(i)) return false; for (int i = 0; i < len; i++) if (raw.byteAt(start + i) != (byte) name.charAt(i)) return false;
return true; return true;
} }
/**
* Percent-decodes a value slice from {@code raw} into a UTF-8 String.
* {@code %XX} triplets are decoded to their byte values; {@code +} decodes as space.
* Invalid {@code %} sequences are passed through as-is.
*/
private String decode(int start, int length) {
byte[] out = new byte[length]; // upper bound — decoded is never longer
int w = 0;
for (int i = 0; i < length; i++) {
byte b = raw.byteAt(start + i);
if (b == '+') {
out[w++] = ' ';
} else if (b == '%' && i + 2 < length) {
int hi = hexVal(raw.byteAt(start + i + 1));
int lo = hexVal(raw.byteAt(start + i + 2));
if (hi >= 0 && lo >= 0) {
out[w++] = (byte) ((hi << 4) | lo);
i += 2;
} else {
out[w++] = b; // not a valid escape — keep literal %
}
} else {
out[w++] = b;
}
}
return new String(out, 0, w, StandardCharsets.UTF_8);
}
private static int hexVal(byte b) {
if (b >= '0' && b <= '9') return b - '0';
if (b >= 'a' && b <= 'f') return b - 'a' + 10;
if (b >= 'A' && b <= 'F') return b - 'A' + 10;
return -1;
}
} }
@@ -112,12 +112,6 @@ public class Response {
public boolean isStreaming() { return stream != null; } public boolean isStreaming() { return stream != null; }
/**
* Pre-encoded status bytes (e.g. {@code "200 OK"}), or {@code null} if the status was set
* via {@link #status(int)} — in which case {@link HttpStatus#bytesForCode} is used as fallback.
*/
public byte[] getStatusBytes() { return statusBytes; }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Internal setters used by HttpServer for handler return values // Internal setters used by HttpServer for handler return values
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
@@ -12,9 +12,16 @@ import lombok.Getter;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
/** /**
* Base router. Each router has a namespace prefix (default {@code "/"}); * Base router. Each router has a namespace prefix (default {@code "/"}).
* Error handlers are scoped to this router. * Error handlers are scoped to this router.
* The namespace is set automatically by {@link GlobalRouter#mount}. *
* <p>Router-level {@link Middleware middlewares} are passed at construction time and
* pre-fused into a single wrapper applied to every handler registered on this router.
* Handler-level middlewares are passed to the {@link #register} / {@link #get} family of
* methods and wrapped <em>inside</em> the router-level chain.
*
* <p>All middleware composition happens once at boot time; the hot-path sees only a plain
* {@link RequestHandler} call with zero allocation and zero lookup overhead.
*/ */
public abstract class AbstractRouter { public abstract class AbstractRouter {
@@ -24,6 +31,12 @@ public abstract class AbstractRouter {
@Getter(AccessLevel.PACKAGE) @Getter(AccessLevel.PACKAGE)
protected byte[] namespaceBytes = new byte[]{ '/' }; protected byte[] namespaceBytes = new byte[]{ '/' };
/**
* Pre-fused router-level middleware, or {@code null} when none were registered.
* A single null-check in {@link #compile} is the only cost when no router middleware exists.
*/
private final Middleware routerMiddleware;
protected SimpleHandler notFoundHandler = new SimpleHandler((req, res) -> { protected SimpleHandler notFoundHandler = new SimpleHandler((req, res) -> {
res.setStatusCode(404); res.setStatusCode(404);
res.setContentType(ContentType.TEXT_HTML); res.setContentType(ContentType.TEXT_HTML);
@@ -36,6 +49,20 @@ public abstract class AbstractRouter {
return ErrorPages.renderException(req, ex); return ErrorPages.renderException(req, ex);
}; };
/**
* Constructs a router with optional router-level middlewares.
* Middlewares are pre-fused at construction time; the first element executes outermost.
*
* @param middlewares zero or more middlewares applied to every handler on this router
*/
protected AbstractRouter(Middleware... middlewares) {
this.routerMiddleware = middlewares.length == 0 ? null
: middlewares.length == 1 ? middlewares[0]
: Middleware.of(middlewares);
}
// ── Internal wiring ───────────────────────────────────────────────────────
SimpleHandler getNotFoundHandler() { return notFoundHandler; } SimpleHandler getNotFoundHandler() { return notFoundHandler; }
ExceptionHandler getExceptionHandler() { return exceptionHandler; } ExceptionHandler getExceptionHandler() { return exceptionHandler; }
void setNamespace(String namespace) { void setNamespace(String namespace) {
@@ -43,6 +70,30 @@ public abstract class AbstractRouter {
this.namespaceBytes = namespace.getBytes(StandardCharsets.UTF_8); this.namespaceBytes = namespace.getBytes(StandardCharsets.UTF_8);
} }
/**
* Compiles a handler with its middleware chain. Called once per handler at registration.
*
* <p>Application order (innermost → outermost):
* <ol>
* <li>handler-level middlewares (passed at the call site)</li>
* <li>router-level middleware (set in the constructor)</li>
* </ol>
*/
private RequestHandler compile(RequestHandler handler, Middleware[] handlerMiddlewares) {
RequestHandler compiled = handler;
// handler-level: wrap from innermost outward; box FunctionalHandler → SimpleHandler once per layer
for (int i = handlerMiddlewares.length - 1; i >= 0; i--)
compiled = new SimpleHandler(handlerMiddlewares[i].wrap(compiled));
// router-level: outermost wrapper, executes first
if (routerMiddleware != null)
compiled = new SimpleHandler(routerMiddleware.wrap(compiled));
return compiled;
}
private static final Middleware[] NO_MIDDLEWARES = new Middleware[0];
// ── Error handler configuration ───────────────────────────────────────────
public AbstractRouter onNotFound(SimpleHandler.FunctionalHandler handler) { public AbstractRouter onNotFound(SimpleHandler.FunctionalHandler handler) {
this.notFoundHandler = new SimpleHandler(handler); this.notFoundHandler = new SimpleHandler(handler);
return this; return this;
@@ -53,36 +104,54 @@ public abstract class AbstractRouter {
return this; return this;
} }
// ── Lambda handler registration ───────────────────────────────────────────
/** /**
* Registers a route relative to this router's namespace. Return a {@link Response} to replace * Registers a lambda handler with optional handler-level middlewares.
* it entirely, any other non-null value to set it as the body, or {@code null} to leave it unchanged. * Router-level middlewares are applied automatically on top.
*/ */
public AbstractRouter register(HttpMethod method, String path, SimpleHandler.FunctionalHandler handler) { public AbstractRouter register(HttpMethod method, String path,
return addRoute(method, PathUtils.sanitize(path), new SimpleHandler(handler)); SimpleHandler.FunctionalHandler handler,
Middleware... middlewares) {
return addRoute(method, PathUtils.sanitize(path),
compile(new SimpleHandler(handler), middlewares));
} }
public AbstractRouter get(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.GET, path, h); } public AbstractRouter get (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.GET, path, h, m); }
public AbstractRouter post(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.POST, path, h); } public AbstractRouter post (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.POST, path, h, m); }
public AbstractRouter put(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.PUT, path, h); } public AbstractRouter put (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PUT, path, h, m); }
public AbstractRouter delete(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.DELETE, path, h); } public AbstractRouter delete (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.DELETE, path, h, m); }
public AbstractRouter patch(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.PATCH, path, h); } public AbstractRouter patch (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PATCH, path, h, m); }
public AbstractRouter options(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.OPTIONS, path, h); } public AbstractRouter options(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.OPTIONS, path, h, m); }
public AbstractRouter head(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.HEAD, path, h); } public AbstractRouter head (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.HEAD, path, h, m); }
public AbstractRouter trace(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.TRACE, path, h); } public AbstractRouter trace (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.TRACE, path, h, m); }
public AbstractRouter connect(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.CONNECT, path, h); } public AbstractRouter connect(String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.CONNECT, path, h, m); }
public AbstractRouter purge(String path, SimpleHandler.FunctionalHandler h) { return register(HttpMethod.PURGE, path, h); } public AbstractRouter purge (String path, SimpleHandler.FunctionalHandler h, Middleware... m) { return register(HttpMethod.PURGE, path, h, m); }
/** Registers a class-based handler; the class must be annotated with {@link Route @Route}. */ // ── Class-based handler registration ─────────────────────────────────────
public AbstractRouter register(RequestHandler handler) {
/**
* Registers a class-based handler with optional handler-level middlewares.
* The class must carry a {@link Route @Route} annotation declaring method and path.
* Router-level middlewares are applied automatically on top.
*/
public AbstractRouter register(RequestHandler handler, Middleware... middlewares) {
Route annotation = handler.getClass().getAnnotation(Route.class); Route annotation = handler.getClass().getAnnotation(Route.class);
if (annotation != null) { if (annotation != null)
addRoute(HttpMethod.valueOf(annotation.method()), annotation.path(), handler); addRoute(annotation.method(), annotation.path(),
} compile(handler, middlewares));
return this; return this;
} }
// ── Routing ───────────────────────────────────────────────────────────────
public abstract RequestHandler route(Request request); public abstract RequestHandler route(Request request);
/**
* Stores a pre-compiled handler in the underlying routing structure.
* The handler passed here is already fully wrapped — implementations must not
* apply any additional middleware logic.
*/
protected abstract AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler); protected abstract AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler);
protected static void setPathParams(Request request, String[] names, ByteView source, int[] starts, int[] lens) { protected static void setPathParams(Request request, String[] names, ByteView source, int[] starts, int[] lens) {
@@ -20,7 +20,14 @@ import java.util.Map;
public class GlobalRouter extends AbstractRouter { public class GlobalRouter extends AbstractRouter {
private final Map<String, AbstractRouter> subRoutersMap = new HashMap<>(); private final Map<String, AbstractRouter> subRoutersMap = new HashMap<>();
private final List<AbstractRouter> sortedSubRouters = new ArrayList<>(); private final List<AbstractRouter> sortedSubRouters = new ArrayList<>();
private final AbstractRouter internalRouter = new FastPathRouterImpl(); private final AbstractRouter internalRouter;
public GlobalRouter(Middleware... middlewares) {
super(middlewares);
this.internalRouter = new FastPathRouterImpl();
}
public GlobalRouter() { this(new Middleware[0]); }
public void mount(String namespace, AbstractRouter router) { public void mount(String namespace, AbstractRouter router) {
String sanitized = PathUtils.sanitize(namespace); String sanitized = PathUtils.sanitize(namespace);
@@ -1,5 +1,7 @@
package dev.relism.routing; package dev.relism.routing;
import dev.relism.http.HttpMethod;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@@ -17,6 +19,6 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
public @interface Route { public @interface Route {
String method() default "GET"; HttpMethod method() default HttpMethod.GET;
String path(); String path();
} }
@@ -9,6 +9,7 @@ import dev.relism.http.HttpMethod;
import dev.relism.models.Request; import dev.relism.models.Request;
import dev.relism.models.RequestHandler; import dev.relism.models.RequestHandler;
import dev.relism.routing.AbstractRouter; import dev.relism.routing.AbstractRouter;
import dev.relism.routing.Middleware;
import dev.relism.routing.PathUtils; import dev.relism.routing.PathUtils;
/** /**
@@ -22,6 +23,9 @@ public class FastPathRouterImpl extends AbstractRouter {
private volatile FastPathRouter<ByteView, RequestHandler> router; private volatile FastPathRouter<ByteView, RequestHandler> router;
private String[] cachedParamNames; private String[] cachedParamNames;
public FastPathRouterImpl(Middleware... middlewares) { super(middlewares); }
public FastPathRouterImpl() { super(); }
private static final class FastPathRouterContext { private static final class FastPathRouterContext {
private static final ThreadLocal<MatchResult<RequestHandler>> RESULT_HOLDER = private static final ThreadLocal<MatchResult<RequestHandler>> RESULT_HOLDER =
ThreadLocal.withInitial(() -> new MatchResult<>(32, 128)); ThreadLocal.withInitial(() -> new MatchResult<>(32, 128));