preparing for another refactoring...
This commit is contained in:
@@ -5,11 +5,12 @@ import dev.relism.http.ContentType;
|
||||
import dev.relism.http.HttpStatus;
|
||||
import dev.relism.models.*;
|
||||
import dev.relism.extension.FlashConfiguration;
|
||||
import dev.relism.routing.GlobalRouter;
|
||||
import dev.relism.routing.AbstractRouter;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -21,32 +22,32 @@ import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Pure I/O transport layer. Owns the {@link ServerSocket}, the virtual-thread executor,
|
||||
* and the keep-alive accept loop. All routing is delegated to the {@link GlobalRouter}
|
||||
* supplied at construction time.
|
||||
* Pure I/O transport layer. Owns the {@link ServerSocket}, the virtual-thread
|
||||
* executor, and the keep-alive accept loop. Routing is delegated to a single
|
||||
* {@link AbstractRouter}.
|
||||
*
|
||||
* <p>This class is package-private — use {@link dev.relism.extension.FlashApp} as the
|
||||
* single entry point for creating and configuring a Flash server.
|
||||
* <p>Package-private : use {@link dev.relism.extension.FlashApp} as the single
|
||||
* entry point.
|
||||
*/
|
||||
@Slf4j
|
||||
class HttpServer implements ServerHandle {
|
||||
|
||||
private final FlashConfiguration configuration;
|
||||
private final ServerSocket serverSocket;
|
||||
private final GlobalRouter globalRouter;
|
||||
private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
|
||||
private final Set<Socket> activeSockets = ConcurrentHashMap.newKeySet();
|
||||
private final ServerSocket serverSocket;
|
||||
private final AbstractRouter router;
|
||||
private final ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
|
||||
private final Set<Socket> activeSockets = ConcurrentHashMap.newKeySet();
|
||||
private volatile boolean stopped = false;
|
||||
private final CompletableFuture<Void> readyFuture = new CompletableFuture<>();
|
||||
|
||||
private static final byte[] HTTP_1_1 = "HTTP/1.1 ".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CONTENT_TYPE = "Content-Type: ".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CONTENT_LENGTH = "Content-Length: ".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CONNECTION_CLOSE = "Connection: close\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] HTTP_1_1 = "HTTP/1.1 ".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CRLF = "\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CONTENT_TYPE = "Content-Type: ".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CONTENT_LENGTH = "Content-Length: ".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CONNECTION_CLOSE = "Connection: close\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] CONNECTION_KEEPALIVE = "Connection: keep-alive\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] TRANSFER_CHUNKED = "Transfer-Encoding: chunked\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] FINAL_CHUNK = "0\r\n\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] TRANSFER_CHUNKED = "Transfer-Encoding: chunked\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] FINAL_CHUNK = "0\r\n\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
private static final byte[] UNKNOWN_STATUS_SUFFIX = " Unknown".getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
private static final byte[][] DIGITS = new byte[10][1];
|
||||
@@ -55,20 +56,12 @@ class HttpServer implements ServerHandle {
|
||||
DIGITS[i] = String.valueOf(i).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the transport with a pre-built router. Called exclusively by
|
||||
* {@link dev.relism.extension.FlashApp}.
|
||||
*
|
||||
* @param configuration server configuration (port, host, buffer sizes)
|
||||
* @param globalRouter the fully-wired router to dispatch requests to
|
||||
*/
|
||||
HttpServer(FlashConfiguration configuration, GlobalRouter globalRouter) throws IOException {
|
||||
HttpServer(FlashConfiguration configuration, AbstractRouter router) throws IOException {
|
||||
this.configuration = configuration;
|
||||
this.serverSocket = new ServerSocket(configuration.getPort());
|
||||
this.globalRouter = globalRouter;
|
||||
this.router = router;
|
||||
}
|
||||
|
||||
/** Returns a future that completes once the accept loop is running and the server is ready. */
|
||||
@Override
|
||||
public CompletableFuture<Void> start() {
|
||||
Thread.ofPlatform().name("flash-accept-loop").daemon(false).start(this::run);
|
||||
@@ -90,15 +83,10 @@ class HttpServer implements ServerHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/** Closes all active connections and shuts down the executor. Returns when complete. */
|
||||
@Override
|
||||
public CompletableFuture<Void> stop() {
|
||||
stopped = true;
|
||||
try {
|
||||
serverSocket.close();
|
||||
} catch (IOException e) {
|
||||
log.error("Error closing server socket", e);
|
||||
}
|
||||
try { serverSocket.close(); } catch (IOException e) { log.error("Error closing server socket", e); }
|
||||
activeSockets.forEach(s -> { try { s.close(); } catch (IOException ignored) {} });
|
||||
executorService.shutdown();
|
||||
try {
|
||||
@@ -111,7 +99,7 @@ class HttpServer implements ServerHandle {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
// ── Hot-path ──────────────────────────────────────────────────────────────
|
||||
// ── Hot-path ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void process(Socket socket) {
|
||||
activeSockets.add(socket);
|
||||
@@ -119,36 +107,32 @@ class HttpServer implements ServerHandle {
|
||||
try (socket;
|
||||
InputStream in = socket.getInputStream();
|
||||
OutputStream out = new BufferedOutputStream(socket.getOutputStream())) {
|
||||
RequestParser parser = new RequestParser(configuration.getMaxHeaderBufferSize());
|
||||
RequestParser parser = new RequestParser(
|
||||
configuration.getMaxHeaderBufferSize(),
|
||||
(InetSocketAddress) socket.getRemoteSocketAddress());
|
||||
while (!stopped) {
|
||||
Request request = parser.parse(in);
|
||||
if (request == null)
|
||||
break;
|
||||
if (request == null) break;
|
||||
|
||||
boolean keepAlive = isKeepAlive(request);
|
||||
|
||||
Response response = new Response(200, ContentType.TEXT_PLAIN);
|
||||
RequestHandler handler = globalRouter.route(request);
|
||||
|
||||
RequestHandler handler = router.route(request);
|
||||
if (handler == null) handler = router.getNotFoundHandler();
|
||||
|
||||
try {
|
||||
Object result = handler.handle(request, response);
|
||||
if (result instanceof Response r)
|
||||
response = r;
|
||||
else if (result != null)
|
||||
response.setBody(result);
|
||||
if (result instanceof Response r) response = r;
|
||||
else if (result != null) response.setBody(result);
|
||||
} catch (Exception ex) {
|
||||
Object result = globalRouter.resolveExceptionHandler(request).handle(ex, request, response);
|
||||
if (result instanceof Response r)
|
||||
response = r;
|
||||
else if (result != null)
|
||||
response.setBody(result);
|
||||
Object result = router.getExceptionHandler().handle(ex, request, response);
|
||||
if (result instanceof Response r) response = r;
|
||||
else if (result != null) response.setBody(result);
|
||||
}
|
||||
|
||||
writeResponse(out, response, keepAlive);
|
||||
request.drain();
|
||||
|
||||
if (!keepAlive)
|
||||
break;
|
||||
if (!keepAlive) break;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (!stopped) {
|
||||
@@ -170,11 +154,6 @@ class HttpServer implements ServerHandle {
|
||||
|| request.headerEquals("Connection", "keep-alive");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a complete HTTP response. The fixed-body path (the common case for simple handlers
|
||||
* like /plaintext) is kept inline; streaming and chunked bodies are delegated to
|
||||
* {@link #writeStreamingBody} so the JIT can optimise this method aggressively.
|
||||
*/
|
||||
private static void writeResponse(OutputStream out, Response response, boolean keepAlive) throws IOException {
|
||||
out.write(HTTP_1_1);
|
||||
byte[] statusBytes = response.getStatusBytes();
|
||||
|
||||
@@ -10,6 +10,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
@@ -21,15 +22,18 @@ import java.util.Arrays;
|
||||
public class RequestParser {
|
||||
private static final int INITIAL_BUFFER_SIZE = 8192;
|
||||
|
||||
private final int maxHeaderBufferSize;
|
||||
private final HeaderMap headerMap = new HeaderMap();
|
||||
private final int maxHeaderBufferSize;
|
||||
private final InetSocketAddress remoteAddress; // set once per connection, never changes
|
||||
private final HeaderMap headerMap = new HeaderMap();
|
||||
private byte[] buffer;
|
||||
private int bufBase = 0; // absolute start of valid data in buffer
|
||||
private int bufLen = 0; // number of valid bytes from bufBase
|
||||
|
||||
public RequestParser() { this(64 * 1024); }
|
||||
public RequestParser(int maxHeaderBufferSize) {
|
||||
public RequestParser() { this(64 * 1024, null); }
|
||||
public RequestParser(int maxHeaderBufferSize) { this(maxHeaderBufferSize, null); }
|
||||
public RequestParser(int maxHeaderBufferSize, InetSocketAddress remoteAddress) {
|
||||
this.maxHeaderBufferSize = maxHeaderBufferSize;
|
||||
this.remoteAddress = remoteAddress;
|
||||
this.buffer = new byte[Math.min(INITIAL_BUFFER_SIZE, maxHeaderBufferSize)];
|
||||
}
|
||||
|
||||
@@ -133,9 +137,9 @@ public class RequestParser {
|
||||
RequestLine requestLine = new RequestLine(method, pathView, queryView, protocolView, headerMap);
|
||||
|
||||
if (isChunked) {
|
||||
return Request.forParsed(requestLine, new ChunkedInputStream(in, buffer, bodyStart, preBufLen), -1L, null, 0, 0);
|
||||
return Request.forParsed(requestLine, new ChunkedInputStream(in, buffer, bodyStart, preBufLen), -1L, null, 0, 0, remoteAddress);
|
||||
}
|
||||
return Request.forParsed(requestLine, in, contentLength, buffer, bodyStart, preBufLen);
|
||||
return Request.forParsed(requestLine, in, contentLength, buffer, bodyStart, preBufLen, remoteAddress);
|
||||
}
|
||||
|
||||
private static int findEndOfHeader(byte[] buf, int from, int len) {
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
package dev.relism;
|
||||
|
||||
import dev.relism.extension.FlashConfiguration;
|
||||
import dev.relism.routing.GlobalRouter;
|
||||
import dev.relism.routing.AbstractRouter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Public handle to the underlying HTTP transport. Returned by {@link #create} so that
|
||||
* {@link dev.relism.extension.FlashApp} can start and stop the server without holding
|
||||
* a direct reference to the package-private {@link HttpServer}.
|
||||
* Public handle to the underlying HTTP transport. Returned by {@link #create}
|
||||
* so that {@link dev.relism.extension.FlashApp} can start and stop the server
|
||||
* without holding a direct reference to the package-private {@link HttpServer}.
|
||||
*/
|
||||
public interface ServerHandle {
|
||||
|
||||
CompletableFuture<Void> start();
|
||||
CompletableFuture<Void> stop();
|
||||
|
||||
/**
|
||||
* Creates the HTTP transport. Called exclusively by
|
||||
* {@link dev.relism.extension.FlashApp}.
|
||||
*/
|
||||
static ServerHandle create(FlashConfiguration config, GlobalRouter router) throws IOException {
|
||||
static ServerHandle create(FlashConfiguration config, AbstractRouter router) throws IOException {
|
||||
return new HttpServer(config, router);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package dev.relism.exceptions;
|
||||
|
||||
public class DuplicateNamespaceException extends RuntimeException {
|
||||
public DuplicateNamespaceException(String namespace) {
|
||||
super("Router with namespace '" + namespace + "' is already registered.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package dev.relism.exceptions;
|
||||
|
||||
/**
|
||||
* Thrown at boot time when Flash detects a configuration or registration error.
|
||||
*
|
||||
* <p>Fail-fast: a clear crash at startup is always preferable to a server that
|
||||
* starts "empty" and silently drops routes.
|
||||
*/
|
||||
public class InitializationException extends RuntimeException {
|
||||
|
||||
public InitializationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InitializationException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import java.util.List;
|
||||
* valid — processors may also use the call purely for side effects
|
||||
* (e.g. collecting OpenAPI metadata).
|
||||
*
|
||||
* <p>Register processors via {@link ExtensionContext#addAnnotationProcessor}.
|
||||
* <p>Register processors via {@link FlashContext#addAnnotationProcessor}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface AnnotationProcessor {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package dev.relism.extension;
|
||||
|
||||
import dev.relism.ServerHandle;
|
||||
import dev.relism.exceptions.InitializationException;
|
||||
import dev.relism.http.HttpMethod;
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.models.SimpleHandler;
|
||||
import dev.relism.routing.AbstractRouter;
|
||||
import dev.relism.routing.GlobalRouter;
|
||||
import dev.relism.routing.Middleware;
|
||||
import dev.relism.routing.Route;
|
||||
import dev.relism.routing.RouteHandle;
|
||||
import dev.relism.routing.routers.fastpathrouter.FastPathRouterImpl;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
@@ -16,98 +17,62 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Primary entry point for Flash. Creates and owns both the {@link GlobalRouter} and
|
||||
* the {@link HttpServer} (pure I/O transport). All route registration goes through
|
||||
* {@code FlashApp} or a {@link FlashScope} — never through the server or router directly.
|
||||
* Single entry point for Flash. Owns one flat {@link FastPathRouterImpl} —
|
||||
* all routes (app-level and scoped) compile into a single FSM at {@link #start()}.
|
||||
*
|
||||
* <p>Create via the static factories:
|
||||
* <pre>{@code
|
||||
* FlashApp app = FlashApp.create(8080);
|
||||
* FlashApp app = FlashApp.create(FlashConfiguration.builder().port(8080).build());
|
||||
* }</pre>
|
||||
* <h3>Deferred routing</h3>
|
||||
* Routes accumulate during the builder phase. At {@code start()}:
|
||||
* <ol>
|
||||
* <li>Global middlewares are prepended to every route</li>
|
||||
* <li>Annotation processors run for class-based handlers</li>
|
||||
* <li>Handlers are bound to the {@link FlashContext}</li>
|
||||
* <li>All routes compile into one FSM — zero prefix scanning at runtime</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>Install extensions, register routes, mount namespaces, then start:
|
||||
* <pre>{@code
|
||||
* FlashApp.create(8080)
|
||||
* .install(new JacksonExtension())
|
||||
* .install(new OidcExtension(config))
|
||||
* .use(cors)
|
||||
* .get("/ping", (req, res) -> "pong")
|
||||
* .get("/me", (req, res) -> ClaimsHolder.user().email()).with(oidc.protect())
|
||||
* .register(new HomePage()) // @Route + annotation processors applied
|
||||
* .scan("dev.example.handlers") // classpath scan, no-arg constructors
|
||||
* .mount("/api", scope -> {
|
||||
* scope.register(new UserHandler()); // @Authenticated works here
|
||||
* scope.get("/health", (req, res) -> "ok");
|
||||
* })
|
||||
* .scan("dev.example.handlers")
|
||||
* .mount("/api", scope -> scope.get("/health", (req, res) -> "ok"))
|
||||
* .start();
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Auto-flush</h3>
|
||||
* Calling any registration method returns a {@link RouteHandle}. Calling
|
||||
* {@link RouteHandle#with} is optional — if omitted, the route is registered
|
||||
* automatically before the next operation or at {@link #start()}. This means
|
||||
* trailing {@code .with()} calls are never required for routes with no middleware.
|
||||
*/
|
||||
public final class FlashApp implements FlashRegistrar {
|
||||
|
||||
private final GlobalRouter router;
|
||||
private final ServerHandle server;
|
||||
private final ExtensionContext ctx = new ExtensionContext();
|
||||
private final AbstractRouter router = new FastPathRouterImpl();
|
||||
private final ServerHandle server;
|
||||
private final FlashContext ctx = new FlashContext();
|
||||
private final List<Middleware> globalMiddlewares = new ArrayList<>();
|
||||
private final List<RouteDefinition> deferredRoutes = new ArrayList<>();
|
||||
|
||||
/** The last returned RouteHandle not yet registered — auto-flushed before the next operation. */
|
||||
private RouteHandle<?> pending;
|
||||
|
||||
/**
|
||||
* Global middlewares applied to every route, regardless of how it is registered
|
||||
* (lambda, class-based, or via {@link #scan}).
|
||||
* Accumulated via {@link #use}; applied outermost in the chain (before injected and
|
||||
* explicit middlewares).
|
||||
*/
|
||||
private final List<Middleware> globalMiddlewares = new ArrayList<>();
|
||||
|
||||
private FlashApp(FlashConfiguration config) {
|
||||
this.router = new GlobalRouter();
|
||||
try {
|
||||
this.server = ServerHandle.create(config, router);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to bind server socket on port " + config.getPort(), e);
|
||||
throw new InitializationException("Failed to bind on port " + config.getPort(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Factories ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Creates a {@code FlashApp} listening on {@code port} with default configuration.
|
||||
*
|
||||
* @param port the TCP port to bind
|
||||
*/
|
||||
public static FlashApp create(int port) {
|
||||
return create(FlashConfiguration.builder().port(port).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@code FlashApp} with full server configuration.
|
||||
*
|
||||
* @param config server configuration (port, host, buffer sizes, etc.)
|
||||
*/
|
||||
public static FlashApp create(FlashConfiguration config) {
|
||||
return new FlashApp(config);
|
||||
}
|
||||
|
||||
// ── Pending flush ─────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Registers any pending route (from the previous {@code get/post/register} call)
|
||||
* with no middleware if it has not already been committed via {@link RouteHandle#with}.
|
||||
*/
|
||||
private void flushPending() {
|
||||
if (pending != null) {
|
||||
pending.ensureRegistered();
|
||||
pending = null;
|
||||
}
|
||||
if (pending != null) { pending.ensureRegistered(); pending = null; }
|
||||
}
|
||||
|
||||
private <P> RouteHandle<P> track(RouteHandle<P> handle) {
|
||||
@@ -116,15 +81,8 @@ public final class FlashApp implements FlashRegistrar {
|
||||
return handle;
|
||||
}
|
||||
|
||||
// ── FlashRegistrar — extension installation ───────────────────────────────
|
||||
// ── Extension installation ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Installs an extension. Extensions receive this {@code FlashApp} as a
|
||||
* {@link FlashRegistrar} so they can register routes and expose services.
|
||||
*
|
||||
* @param ext the extension to install
|
||||
* @return {@code this} for chaining
|
||||
*/
|
||||
@Override
|
||||
public FlashApp install(FlashExtension ext) {
|
||||
flushPending();
|
||||
@@ -132,34 +90,12 @@ public final class FlashApp implements FlashRegistrar {
|
||||
return this;
|
||||
}
|
||||
|
||||
// ── Global middleware ──────────────────────────────────────────────────────
|
||||
// ── Global middleware ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Registers one or more global middlewares applied to <em>every</em> route on this app,
|
||||
* regardless of how the route is registered (lambda, class-based, or via {@link #scan}).
|
||||
*
|
||||
* <p>Global middlewares execute outermost — before annotation-injected middlewares
|
||||
* (e.g. {@code @Authenticated}) and before any explicit {@link RouteHandle#with} chain.
|
||||
* Execution order mirrors the declaration order: the first argument wraps everything else.
|
||||
*
|
||||
* <p>Must be called before {@link #start()}. Calling {@code use} after routes have already
|
||||
* been registered will not retroactively affect those routes.
|
||||
*
|
||||
* <pre>{@code
|
||||
* Middleware cors = next -> (req, res) -> {
|
||||
* res.header("Access-Control-Allow-Origin", "*");
|
||||
* if (req.method() == HttpMethod.OPTIONS) { res.status(204); return null; }
|
||||
* return next.handle(req, res);
|
||||
* };
|
||||
*
|
||||
* FlashApp.create(8080)
|
||||
* .use(cors)
|
||||
* .scan("dev.example.handlers")
|
||||
* .start();
|
||||
* }</pre>
|
||||
*
|
||||
* @param middlewares one or more middlewares to apply globally
|
||||
* @return {@code this} for chaining
|
||||
* Registers global middlewares applied to <em>every</em> route — including
|
||||
* routes registered before this call and routes inside mounted scopes.
|
||||
* Order-independent: resolved at {@link #start()}.
|
||||
*/
|
||||
public FlashApp use(Middleware... middlewares) {
|
||||
flushPending();
|
||||
@@ -167,182 +103,157 @@ public final class FlashApp implements FlashRegistrar {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends global middlewares to an explicit per-route array.
|
||||
* Returns {@code explicit} unchanged when no global middlewares have been registered
|
||||
* (zero-allocation fast path).
|
||||
*/
|
||||
private Middleware[] withGlobal(Middleware[] explicit) {
|
||||
if (globalMiddlewares.isEmpty()) return explicit;
|
||||
return Stream.concat(globalMiddlewares.stream(), Arrays.stream(explicit))
|
||||
.toArray(Middleware[]::new);
|
||||
}
|
||||
// ── Route registration (deferred) ─────────────────────────────────────────
|
||||
|
||||
// ── FlashRegistrar — route registration ───────────────────────────────────
|
||||
@Override public RouteHandle<FlashApp> get (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.GET, path, h); }
|
||||
@Override public RouteHandle<FlashApp> post (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.POST, path, h); }
|
||||
@Override public RouteHandle<FlashApp> put (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.PUT, path, h); }
|
||||
@Override public RouteHandle<FlashApp> delete (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.DELETE, path, h); }
|
||||
@Override public RouteHandle<FlashApp> patch (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.PATCH, path, h); }
|
||||
@Override public RouteHandle<FlashApp> options(String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.OPTIONS, path, h); }
|
||||
@Override public RouteHandle<FlashApp> head (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.HEAD, path, h); }
|
||||
@Override public RouteHandle<FlashApp> trace (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.TRACE, path, h); }
|
||||
@Override public RouteHandle<FlashApp> connect(String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.CONNECT, path, h); }
|
||||
@Override public RouteHandle<FlashApp> purge (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.PURGE, path, h); }
|
||||
|
||||
@Override public RouteHandle<FlashApp> get (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.GET, path, h); }
|
||||
@Override public RouteHandle<FlashApp> post (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.POST, path, h); }
|
||||
@Override public RouteHandle<FlashApp> put (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.PUT, path, h); }
|
||||
@Override public RouteHandle<FlashApp> delete (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.DELETE, path, h); }
|
||||
@Override public RouteHandle<FlashApp> patch (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.PATCH, path, h); }
|
||||
@Override public RouteHandle<FlashApp> options(String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.OPTIONS, path, h); }
|
||||
@Override public RouteHandle<FlashApp> head (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.HEAD, path, h); }
|
||||
@Override public RouteHandle<FlashApp> trace (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.TRACE, path, h); }
|
||||
@Override public RouteHandle<FlashApp> connect(String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.CONNECT, path, h); }
|
||||
@Override public RouteHandle<FlashApp> purge (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.PURGE, path, h); }
|
||||
private static final Middleware[] NO_MW = new Middleware[0];
|
||||
|
||||
private RouteHandle<FlashApp> routeLambda(HttpMethod method, String path, SimpleHandler.FunctionalHandler h) {
|
||||
return track(new RouteHandle<>(this, m -> {
|
||||
Middleware[] all = withGlobal(m);
|
||||
emit(method, path, null, List.of(), all);
|
||||
router.doRegister(method, path, h, all);
|
||||
}));
|
||||
private RouteHandle<FlashApp> lambda(HttpMethod method, String path, SimpleHandler.FunctionalHandler h) {
|
||||
return track(new RouteHandle<>(this, mw ->
|
||||
deferredRoutes.add(new RouteDefinition(method, path, new SimpleHandler(h), NO_MW, mw, false, ctx, "/"))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins registration of a class-based handler. The class must carry a
|
||||
* {@link Route @Route} annotation. All registered {@link AnnotationProcessor}s
|
||||
* are run (e.g. to inject {@code @Authenticated} / {@code @RolesAllowed} middleware).
|
||||
* Injected middlewares are prepended outermost to any explicit ones passed via
|
||||
* {@link RouteHandle#with}.
|
||||
*
|
||||
* <p>Calling {@link RouteHandle#with} is optional — the route is registered
|
||||
* automatically before the next operation or at {@link #start()}.
|
||||
* Registers a class-based handler annotated with {@link Route @Route}.
|
||||
* App-level only — not part of {@link FlashRegistrar}.
|
||||
*/
|
||||
@Override
|
||||
public RouteHandle<FlashApp> register(RequestHandler handler) {
|
||||
return track(new RouteHandle<>(this, explicit -> {
|
||||
List<Middleware> injected = ctx.processors().stream()
|
||||
.flatMap(p -> p.process(handler.getClass()).stream())
|
||||
.toList();
|
||||
Middleware[] all = Stream.concat(
|
||||
globalMiddlewares.stream(),
|
||||
Stream.concat(injected.stream(), Arrays.stream(explicit))
|
||||
).toArray(Middleware[]::new);
|
||||
Route ann = handler.getClass().getAnnotation(Route.class);
|
||||
if (ann != null) emit(ann.method(), ann.path(), handler.getClass(), injected, withGlobal(explicit));
|
||||
router.doRegister(handler, all);
|
||||
}));
|
||||
Route ann = handler.getClass().getAnnotation(Route.class);
|
||||
if (ann == null)
|
||||
throw new InitializationException(
|
||||
handler.getClass().getName() + " is missing @Route");
|
||||
return track(new RouteHandle<>(this, mw ->
|
||||
deferredRoutes.add(new RouteDefinition(ann.method(), ann.path(), handler, NO_MW, mw, true, ctx, "/"))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans {@code packageName} for classes that extend {@link RequestHandler} and
|
||||
* carry {@link Route @Route}. Each is instantiated via its no-arg constructor,
|
||||
* run through annotation processors, and registered.
|
||||
*
|
||||
* <pre>{@code
|
||||
* FlashApp.create(8080)
|
||||
* .install(new OidcExtension(config))
|
||||
* .scan("dev.example.handlers"); // @Authenticated / @RolesAllowed auto-applied
|
||||
* }</pre>
|
||||
*/
|
||||
@Override
|
||||
public FlashApp scan(String packageName) {
|
||||
flushPending();
|
||||
PackageScanner.findHandlers(packageName).forEach(cls -> register(instantiate(cls)).ensureRegistered());
|
||||
PackageScanner.findHandlers(packageName).forEach(cls ->
|
||||
register(instantiate(cls)).ensureRegistered());
|
||||
return this;
|
||||
}
|
||||
|
||||
// ── Namespace mounting ────────────────────────────────────────────────────
|
||||
// ── Namespace mounting (syntactic sugar — routes go into same flat router) ─
|
||||
|
||||
/**
|
||||
* Mounts a scoped sub-router under {@code namespace}. The {@code configure} consumer
|
||||
* receives a {@link FlashScope} that has its own child {@link ExtensionContext}
|
||||
* inheriting all parent services and annotation processors.
|
||||
*
|
||||
* <p>Routes registered on the scope automatically get the namespace prefix prepended.
|
||||
* Annotation processors (e.g. from OIDC) apply identically inside the scope.
|
||||
*
|
||||
* <pre>{@code
|
||||
* app.mount("/api", scope -> {
|
||||
* scope.register(new UserHandler()); // @Authenticated works
|
||||
* scope.get("/health", (req, res) -> "ok");
|
||||
* scope.scan("dev.example.api");
|
||||
* });
|
||||
* }</pre>
|
||||
*
|
||||
* @param namespace the path prefix (e.g. {@code "/api"})
|
||||
* @param configure consumer that registers routes on the scope
|
||||
* Mounts a scoped group of routes under {@code namespace}. The scope is a
|
||||
* pure builder — it prepends the namespace to each path and collects
|
||||
* {@link RouteDefinition}s that merge into this app's single flat router.
|
||||
*/
|
||||
public FlashApp mount(String namespace, Consumer<FlashScope> configure) {
|
||||
flushPending();
|
||||
FlashScope scope = new FlashScope(new dev.relism.routing.routers.fastpathrouter.FastPathRouterImpl(),
|
||||
namespace, ctx);
|
||||
FlashScope scope = new FlashScope(namespace, ctx);
|
||||
configure.accept(scope);
|
||||
scope.flush();
|
||||
router.mount(namespace, scope.router());
|
||||
deferredRoutes.addAll(scope.routes());
|
||||
return this;
|
||||
}
|
||||
|
||||
// ── FlashRegistrar — error handlers ──────────────────────────────────────
|
||||
// ── Error handlers ────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public FlashApp onException(AbstractRouter.ExceptionHandler handler) {
|
||||
flushPending();
|
||||
router.onException(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlashApp onNotFound(SimpleHandler.FunctionalHandler handler) {
|
||||
flushPending();
|
||||
router.onNotFound(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
// ── FlashRegistrar — context ──────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public ExtensionContext ctx() {
|
||||
return ctx;
|
||||
}
|
||||
public FlashContext ctx() { return ctx; }
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Flushes any pending route registration and starts the HTTP server.
|
||||
*
|
||||
* @return a future that completes once the accept loop is running
|
||||
* Compiles all deferred routes into the flat FSM router, then starts
|
||||
* the HTTP transport. One pass, one router, zero prefix scanning.
|
||||
*/
|
||||
public CompletableFuture<Void> start() {
|
||||
flushPending();
|
||||
compile();
|
||||
return server.start();
|
||||
}
|
||||
|
||||
/** Stops the HTTP server and closes all active connections. */
|
||||
public CompletableFuture<Void> stop() {
|
||||
return server.stop();
|
||||
public CompletableFuture<Void> stop() { return server.stop(); }
|
||||
|
||||
// ── Compilation ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compiles all deferred routes. Middleware chain order:
|
||||
* Global → Scope → Annotation (class-based only) → Explicit (.with).
|
||||
*/
|
||||
private void compile() {
|
||||
for (RouteDefinition def : deferredRoutes) {
|
||||
List<Middleware> injected;
|
||||
if (def.classBasedHandler()) {
|
||||
injected = def.ctx().processors().stream()
|
||||
.flatMap(p -> p.process(def.handler().getClass()).stream())
|
||||
.toList();
|
||||
def.handler().bind(def.ctx());
|
||||
} else {
|
||||
injected = List.of();
|
||||
}
|
||||
|
||||
Middleware[] all = concat(globalMiddlewares, def.scopeMiddlewares(),
|
||||
injected, def.explicitMiddlewares());
|
||||
emitEvent(def, all);
|
||||
router.doRegister(def.method(), def.path(), def.handler(), all);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void emitEvent(RouteDefinition def, Middleware[] allMiddlewares) {
|
||||
List<RouteListener> listeners = def.ctx().routeListeners();
|
||||
if (listeners.isEmpty()) return;
|
||||
|
||||
List<Class<? extends Middleware>> chain = new ArrayList<>(allMiddlewares.length);
|
||||
for (Middleware m : allMiddlewares)
|
||||
chain.add((Class<? extends Middleware>) m.getClass());
|
||||
|
||||
Class<?> handlerClass = def.classBasedHandler() ? def.handler().getClass() : null;
|
||||
RouteEvent event = new RouteEvent(def.method(), def.path(), def.namespace(),
|
||||
"FlashApp", handlerClass, List.copyOf(chain));
|
||||
listeners.forEach(l -> l.onRoute(event));
|
||||
}
|
||||
|
||||
// ── Internals ─────────────────────────────────────────────────────────────
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Middleware[] concat(List<Middleware> global, Middleware[] scope,
|
||||
List<Middleware> injected, Middleware[] explicit) {
|
||||
int total = global.size() + scope.length + injected.size() + explicit.length;
|
||||
if (total == 0) return NO_MW;
|
||||
if (total == explicit.length && scope.length == 0) return explicit;
|
||||
Middleware[] all = new Middleware[total];
|
||||
int i = 0;
|
||||
for (Middleware m : global) all[i++] = m;
|
||||
for (Middleware m : scope) all[i++] = m;
|
||||
for (Middleware m : injected) all[i++] = m;
|
||||
System.arraycopy(explicit, 0, all, i, explicit.length);
|
||||
return all;
|
||||
}
|
||||
|
||||
private static RequestHandler instantiate(Class<?> cls) {
|
||||
try {
|
||||
return (RequestHandler) cls.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to instantiate handler: " + cls.getName() +
|
||||
throw new InitializationException(
|
||||
"Failed to instantiate " + cls.getName() +
|
||||
" — ensure it has a public no-arg constructor", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a {@link RouteEvent} to all registered {@link RouteListener}s.
|
||||
* No-op if no listener has been registered (fast empty-list check).
|
||||
* Called once per route at boot time — never on the request hot-path.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void emit(HttpMethod method, String path, Class<?> handlerClass,
|
||||
List<Middleware> injected, Middleware[] explicit) {
|
||||
List<RouteListener> listeners = ctx.routeListeners();
|
||||
if (listeners.isEmpty()) return;
|
||||
Middleware[] routerMws = router.routerMiddlewares();
|
||||
List<Class<? extends Middleware>> chain = new ArrayList<>(routerMws.length + injected.size() + explicit.length);
|
||||
for (Middleware m : routerMws) chain.add((Class<? extends Middleware>) m.getClass());
|
||||
for (Middleware m : injected) chain.add((Class<? extends Middleware>) m.getClass());
|
||||
for (Middleware m : explicit) chain.add((Class<? extends Middleware>) m.getClass());
|
||||
RouteEvent event = new RouteEvent(method, path, router.getNamespace(),
|
||||
router.getClass().getSimpleName(), handlerClass, List.copyOf(chain));
|
||||
listeners.forEach(l -> l.onRoute(event));
|
||||
}
|
||||
}
|
||||
|
||||
+38
-46
@@ -4,46 +4,42 @@ import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Shared registry passed to every extension during {@link FlashExtension#install}.
|
||||
* Extensions use it in two ways:
|
||||
* Central service registry and boot-time hook coordinator.
|
||||
*
|
||||
* <p>Every handler, extension, and scope shares one (or a child of one) {@code FlashContext}.
|
||||
* It provides three capabilities:
|
||||
* <ol>
|
||||
* <li><b>Service sharing</b> — provide/require typed objects (e.g. {@code ObjectMapper},
|
||||
* {@code OpenApiBuilder}) so extensions can build on each other.</li>
|
||||
* <li><b>Annotation processing</b> — register {@link AnnotationProcessor}s that
|
||||
* are invoked for every handler, injecting middleware derived from
|
||||
* annotations ({@code @RolesAllowed}, {@code @Authenticated}, etc.).</li>
|
||||
* <li><b>Service registry</b> — typed {@link #provide}/{@link #require}/{@link #find}.</li>
|
||||
* <li><b>Annotation processors</b> — middleware injection from handler annotations.</li>
|
||||
* <li><b>Route listeners</b> — boot-time observation of the route graph.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>A child context (created via {@link #child()}) inherits all services and processors
|
||||
* from its parent. Services provided and processors added on the child are scoped to it
|
||||
* and not visible in the parent or sibling scopes.
|
||||
* <p>A child context (via {@link #child()}) inherits parent services and processors.
|
||||
* Services provided on the child are scoped and invisible to the parent.
|
||||
*/
|
||||
public class ExtensionContext {
|
||||
public class FlashContext {
|
||||
|
||||
private final ExtensionContext parent;
|
||||
private final FlashContext parent;
|
||||
private final Map<Class<?>, Object> registry = new LinkedHashMap<>();
|
||||
private final List<AnnotationProcessor> processors = new ArrayList<>();
|
||||
private final List<RouteListener> routeListeners = new ArrayList<>();
|
||||
|
||||
public ExtensionContext() {
|
||||
public FlashContext() {
|
||||
this.parent = null;
|
||||
}
|
||||
|
||||
private ExtensionContext(ExtensionContext parent) {
|
||||
private FlashContext(FlashContext parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a child context that inherits this context's services and processors.
|
||||
* Services provided and processors added on the child do not affect the parent.
|
||||
*/
|
||||
public ExtensionContext child() {
|
||||
return new ExtensionContext(this);
|
||||
/** Creates a child context that inherits this context's services and processors. */
|
||||
public FlashContext child() {
|
||||
return new FlashContext(this);
|
||||
}
|
||||
|
||||
// ── Service registry ─────────────────────────────────────────────────────
|
||||
|
||||
/** Stores {@code instance} under {@code type} for retrieval by other extensions. */
|
||||
/** Stores {@code instance} under {@code type} for retrieval via {@link #require} or {@link #find}. */
|
||||
public <T> void provide(Class<T> type, T instance) {
|
||||
registry.put(type, instance);
|
||||
}
|
||||
@@ -51,7 +47,8 @@ public class ExtensionContext {
|
||||
/**
|
||||
* Retrieves the service registered under {@code type}.
|
||||
* Checks own scope first, then the parent chain.
|
||||
* Throws {@link IllegalStateException} if not found — install order matters.
|
||||
*
|
||||
* @throws IllegalStateException if not found
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T require(Class<T> type) {
|
||||
@@ -59,12 +56,12 @@ public class ExtensionContext {
|
||||
if (val == null && parent != null) val = parent.find(type).orElse(null);
|
||||
if (val == null)
|
||||
throw new IllegalStateException(
|
||||
"Extension dependency not found: " + type.getSimpleName() +
|
||||
" — install the required extension first");
|
||||
"Service not found: " + type.getSimpleName() +
|
||||
" — provide it via FlashContext.provide() or install the required extension");
|
||||
return val;
|
||||
}
|
||||
|
||||
/** Returns the service under {@code type}, or empty if not installed in this scope or any parent. */
|
||||
/** Returns the service under {@code type}, or empty if not provided in this scope or any parent. */
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Optional<T> find(Class<T> type) {
|
||||
T val = (T) registry.get(type);
|
||||
@@ -72,21 +69,26 @@ public class ExtensionContext {
|
||||
return parent != null ? parent.find(type) : Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the service registered under {@code type} as an {@link Optional},
|
||||
* or {@link Optional#empty()} if not present in this scope or any parent.
|
||||
*
|
||||
* <p>Semantically identical to {@link #find} — prefer this name for expressive call sites
|
||||
* ({@code ctx.optional(ViewEngine.class).ifPresent(...)}). Respects the parent-first
|
||||
* scope hierarchy: own registry is checked first, then the parent chain.
|
||||
*/
|
||||
public <T> Optional<T> optional(Class<T> type) {
|
||||
return find(type);
|
||||
}
|
||||
|
||||
// ── Annotation processors ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Registers an {@link AnnotationProcessor}. Called by extensions during
|
||||
* {@link FlashExtension#install}. Processors are invoked in registration order
|
||||
* (parent processors first, then own).
|
||||
*/
|
||||
/** Registers an {@link AnnotationProcessor}. Processors run once per class-based handler at boot. */
|
||||
public void addAnnotationProcessor(AnnotationProcessor processor) {
|
||||
processors.add(processor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all processors visible from this context: parent processors first,
|
||||
* then processors added directly to this context.
|
||||
*/
|
||||
/** All processors visible from this context: parent-first, then own. */
|
||||
List<AnnotationProcessor> processors() {
|
||||
if (parent == null) return Collections.unmodifiableList(processors);
|
||||
List<AnnotationProcessor> parentProcessors = parent.processors();
|
||||
@@ -96,22 +98,12 @@ public class ExtensionContext {
|
||||
|
||||
// ── Route listeners ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Registers a {@link RouteListener} that will be notified once for every route
|
||||
* registered on this context's {@link dev.relism.extension.FlashApp} or any
|
||||
* {@link dev.relism.extension.FlashScope} that inherits from it.
|
||||
*
|
||||
* <p>Call this inside {@link FlashExtension#install} to observe all routes.
|
||||
* If no listener is registered the emission path is a no-op.
|
||||
*/
|
||||
/** Registers a boot-time {@link RouteListener}. Zero overhead on the request hot-path. */
|
||||
public void addRouteListener(RouteListener listener) {
|
||||
routeListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all route listeners visible from this context: parent listeners first,
|
||||
* then listeners added directly to this context.
|
||||
*/
|
||||
/** All route listeners visible from this context: parent-first, then own. */
|
||||
List<RouteListener> routeListeners() {
|
||||
if (parent == null) return Collections.unmodifiableList(routeListeners);
|
||||
List<RouteListener> parentListeners = parent.routeListeners();
|
||||
@@ -3,17 +3,17 @@ package dev.relism.extension;
|
||||
/**
|
||||
* Contract for all Flash extensions. An extension receives a {@link FlashRegistrar}
|
||||
* (either a {@link FlashApp} or a {@link FlashScope}) so it can register routes and
|
||||
* expose shared services via {@link ExtensionContext}.
|
||||
* expose shared services via {@link FlashContext}.
|
||||
*
|
||||
* <p>Extensions work identically whether installed at the top-level app or inside a
|
||||
* mounted scope:
|
||||
*
|
||||
* <pre>{@code
|
||||
* public class RateLimitExtension implements FlashExtension {
|
||||
* public void install(FlashRegistrar app, ExtensionContext ctx) {
|
||||
* public void install(FlashRegistrar app, FlashContext ctx) {
|
||||
* RateLimiter limiter = new RateLimiter(100);
|
||||
* ctx.provide(RateLimiter.class, limiter);
|
||||
* app.onException((ex, req, res) -> { ... });
|
||||
* app.get("/rate-info", (req, res) -> limiter.info());
|
||||
* }
|
||||
* }
|
||||
*
|
||||
@@ -28,5 +28,5 @@ package dev.relism.extension;
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface FlashExtension {
|
||||
void install(FlashRegistrar app, ExtensionContext ctx);
|
||||
void install(FlashRegistrar app, FlashContext ctx);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
package dev.relism.extension;
|
||||
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.models.SimpleHandler;
|
||||
import dev.relism.routing.AbstractRouter;
|
||||
import dev.relism.routing.RouteHandle;
|
||||
|
||||
/**
|
||||
* Common registration surface shared by {@link FlashApp} and {@link FlashScope}.
|
||||
*
|
||||
* <p>{@link FlashExtension#install} receives a {@code FlashRegistrar} so that extensions
|
||||
* <p>{@link FlashExtension#install} receives a {@code FlashRegistrar} so extensions
|
||||
* work identically whether installed at the top-level app or inside a mounted scope.
|
||||
*
|
||||
* <p>Route registration follows the auto-flush pattern: calling any registration method
|
||||
* without a subsequent {@link RouteHandle#with} is equivalent to calling
|
||||
* {@code .with()} with no arguments — the route is registered with no middleware.
|
||||
*
|
||||
* <pre>{@code
|
||||
* // In an extension:
|
||||
* public void install(FlashRegistrar app, ExtensionContext ctx) {
|
||||
* public void install(FlashRegistrar app, FlashContext ctx) {
|
||||
* app.get("/health", (req, res) -> "ok");
|
||||
* app.get("/secured", (req, res) -> user()).with(oidc.protect());
|
||||
* }
|
||||
@@ -25,11 +19,9 @@ import dev.relism.routing.RouteHandle;
|
||||
*/
|
||||
public interface FlashRegistrar {
|
||||
|
||||
// ── Extension installation ────────────────────────────────────────────────
|
||||
|
||||
FlashRegistrar install(FlashExtension ext);
|
||||
|
||||
// ── Route registration ────────────────────────────────────────────────────
|
||||
// ── Lambda route registration ────────────────────────────────────────────
|
||||
|
||||
RouteHandle<?> get (String path, SimpleHandler.FunctionalHandler h);
|
||||
RouteHandle<?> post (String path, SimpleHandler.FunctionalHandler h);
|
||||
@@ -43,29 +35,13 @@ public interface FlashRegistrar {
|
||||
RouteHandle<?> purge (String path, SimpleHandler.FunctionalHandler h);
|
||||
|
||||
/**
|
||||
* Begins registration of a class-based handler. The class must carry a
|
||||
* {@link dev.relism.routing.Route @Route} annotation. Annotation processors
|
||||
* (e.g. {@code @Authenticated}, {@code @RolesAllowed}) are applied automatically.
|
||||
*
|
||||
* <p>Calling {@link RouteHandle#with} is optional — the route is registered
|
||||
* automatically before the next operation or at {@code start()}.
|
||||
*/
|
||||
RouteHandle<?> register(RequestHandler h);
|
||||
|
||||
/**
|
||||
* Scans {@code packageName} for classes that extend {@link RequestHandler} and
|
||||
* carry {@link dev.relism.routing.Route @Route}. Each is instantiated via its
|
||||
* Scans {@code packageName} for classes that extend
|
||||
* {@link dev.relism.models.RequestHandler} and carry
|
||||
* {@link dev.relism.routing.Route @Route}. Each is instantiated via its
|
||||
* no-arg constructor, run through annotation processors, and registered.
|
||||
*/
|
||||
FlashRegistrar scan(String packageName);
|
||||
|
||||
// ── Error handlers ────────────────────────────────────────────────────────
|
||||
|
||||
FlashRegistrar onException(AbstractRouter.ExceptionHandler h);
|
||||
FlashRegistrar onNotFound(SimpleHandler.FunctionalHandler h);
|
||||
|
||||
// ── Context access ────────────────────────────────────────────────────────
|
||||
|
||||
/** Returns the {@link ExtensionContext} for this registrar (app or scope). */
|
||||
ExtensionContext ctx();
|
||||
/** Returns the {@link FlashContext} for this registrar. */
|
||||
FlashContext ctx();
|
||||
}
|
||||
|
||||
@@ -1,62 +1,43 @@
|
||||
package dev.relism.extension;
|
||||
|
||||
import dev.relism.exceptions.InitializationException;
|
||||
import dev.relism.http.HttpMethod;
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.models.SimpleHandler;
|
||||
import dev.relism.routing.AbstractRouter;
|
||||
import dev.relism.routing.Middleware;
|
||||
import dev.relism.routing.PathUtils;
|
||||
import dev.relism.routing.Route;
|
||||
import dev.relism.routing.RouteHandle;
|
||||
import dev.relism.routing.Middleware;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Scoped registration context for a mounted sub-router namespace.
|
||||
*
|
||||
* <p>Obtained via {@link FlashApp#mount(String, java.util.function.Consumer)}.
|
||||
* A scope has its own child {@link ExtensionContext} that inherits all services and
|
||||
* annotation processors from the parent app, so extensions like {@code @Authenticated}
|
||||
* and {@code @RolesAllowed} work identically inside a scope.
|
||||
*
|
||||
* <p>Extensions installed on a scope are scoped to that namespace and not visible
|
||||
* in the parent or sibling scopes.
|
||||
* Scoped builder for namespace-prefixed routes. Pure syntactic sugar —
|
||||
* does not own a router. Collected routes merge into {@link FlashApp}'s
|
||||
* single flat router at {@link FlashApp#start()}.
|
||||
*
|
||||
* <pre>{@code
|
||||
* app.mount("/api", scope -> {
|
||||
* scope.install(new RateLimitExtension());
|
||||
* scope.register(new UserHandler()); // @Authenticated auto-injected
|
||||
* scope.get("/health", (req, res) -> "ok");
|
||||
* scope.use(authMiddleware);
|
||||
* scope.get("/health", (req, res) -> "ok"); // → GET /api/health
|
||||
* scope.scan("dev.example.api.handlers");
|
||||
* });
|
||||
* }</pre>
|
||||
*/
|
||||
public final class FlashScope implements FlashRegistrar {
|
||||
|
||||
private final AbstractRouter router;
|
||||
private final String namespace;
|
||||
private final ExtensionContext ctx;
|
||||
private final String namespace;
|
||||
private final FlashContext ctx;
|
||||
private final List<Middleware> scopeMiddlewares = new ArrayList<>();
|
||||
private final List<RouteDefinition> deferredRoutes = new ArrayList<>();
|
||||
|
||||
private static final Middleware[] NO_MW = new Middleware[0];
|
||||
|
||||
/** Pending RouteHandle awaiting .with() — auto-flushed before each new registration. */
|
||||
private RouteHandle<?> pending;
|
||||
|
||||
/**
|
||||
* Package-private — only {@link FlashApp} creates scopes.
|
||||
*
|
||||
* @param router the sub-router that will receive routes registered on this scope
|
||||
* @param namespace the namespace prefix (e.g. {@code "/api"})
|
||||
* @param parentCtx the parent app's ExtensionContext — a child is created from it
|
||||
*/
|
||||
FlashScope(AbstractRouter router, String namespace, ExtensionContext parentCtx) {
|
||||
this.router = router;
|
||||
FlashScope(String namespace, FlashContext parentCtx) {
|
||||
this.namespace = namespace;
|
||||
this.ctx = parentCtx.child();
|
||||
}
|
||||
@@ -64,10 +45,7 @@ public final class FlashScope implements FlashRegistrar {
|
||||
// ── Pending flush ─────────────────────────────────────────────────────────
|
||||
|
||||
private void flushPending() {
|
||||
if (pending != null) {
|
||||
pending.ensureRegistered();
|
||||
pending = null;
|
||||
}
|
||||
if (pending != null) { pending.ensureRegistered(); pending = null; }
|
||||
}
|
||||
|
||||
private <P> RouteHandle<P> track(RouteHandle<P> handle) {
|
||||
@@ -76,12 +54,20 @@ public final class FlashScope implements FlashRegistrar {
|
||||
return handle;
|
||||
}
|
||||
|
||||
// ── FlashRegistrar — extension installation ───────────────────────────────
|
||||
// ── Scope middleware ──────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Installs an extension scoped to this namespace.
|
||||
* The extension registers routes and services on this scope only.
|
||||
* Adds middlewares applied to every route in this scope.
|
||||
* Combined at compile time in order: Global → Scope → Annotation → Explicit.
|
||||
*/
|
||||
public FlashScope use(Middleware... middlewares) {
|
||||
flushPending();
|
||||
scopeMiddlewares.addAll(Arrays.asList(middlewares));
|
||||
return this;
|
||||
}
|
||||
|
||||
// ── Extension installation ────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
public FlashScope install(FlashExtension ext) {
|
||||
flushPending();
|
||||
@@ -89,128 +75,73 @@ public final class FlashScope implements FlashRegistrar {
|
||||
return this;
|
||||
}
|
||||
|
||||
// ── FlashRegistrar — route registration ───────────────────────────────────
|
||||
// ── Route registration (deferred) ─────────────────────────────────────────
|
||||
|
||||
@Override public RouteHandle<FlashScope> get (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.GET, path, h); }
|
||||
@Override public RouteHandle<FlashScope> post (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.POST, path, h); }
|
||||
@Override public RouteHandle<FlashScope> put (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.PUT, path, h); }
|
||||
@Override public RouteHandle<FlashScope> delete (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.DELETE, path, h); }
|
||||
@Override public RouteHandle<FlashScope> patch (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.PATCH, path, h); }
|
||||
@Override public RouteHandle<FlashScope> options(String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.OPTIONS, path, h); }
|
||||
@Override public RouteHandle<FlashScope> head (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.HEAD, path, h); }
|
||||
@Override public RouteHandle<FlashScope> trace (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.TRACE, path, h); }
|
||||
@Override public RouteHandle<FlashScope> connect(String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.CONNECT, path, h); }
|
||||
@Override public RouteHandle<FlashScope> purge (String path, SimpleHandler.FunctionalHandler h) { return routeLambda(HttpMethod.PURGE, path, h); }
|
||||
@Override public RouteHandle<FlashScope> get (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.GET, path, h); }
|
||||
@Override public RouteHandle<FlashScope> post (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.POST, path, h); }
|
||||
@Override public RouteHandle<FlashScope> put (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.PUT, path, h); }
|
||||
@Override public RouteHandle<FlashScope> delete (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.DELETE, path, h); }
|
||||
@Override public RouteHandle<FlashScope> patch (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.PATCH, path, h); }
|
||||
@Override public RouteHandle<FlashScope> options(String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.OPTIONS, path, h); }
|
||||
@Override public RouteHandle<FlashScope> head (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.HEAD, path, h); }
|
||||
@Override public RouteHandle<FlashScope> trace (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.TRACE, path, h); }
|
||||
@Override public RouteHandle<FlashScope> connect(String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.CONNECT, path, h); }
|
||||
@Override public RouteHandle<FlashScope> purge (String path, SimpleHandler.FunctionalHandler h) { return lambda(HttpMethod.PURGE, path, h); }
|
||||
|
||||
private RouteHandle<FlashScope> routeLambda(HttpMethod method, String path, SimpleHandler.FunctionalHandler h) {
|
||||
return track(new RouteHandle<>(this, m -> {
|
||||
String full = ns(path);
|
||||
emit(method, full, null, List.of(), m);
|
||||
router.doRegister(method, full, h, m);
|
||||
}));
|
||||
private RouteHandle<FlashScope> lambda(HttpMethod method, String path, SimpleHandler.FunctionalHandler h) {
|
||||
String full = ns(path);
|
||||
Middleware[] scopeMw = snapshotScopeMiddlewares();
|
||||
return track(new RouteHandle<>(this, mw ->
|
||||
deferredRoutes.add(new RouteDefinition(method, full, new SimpleHandler(h), scopeMw, mw, false, ctx, namespace))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Begins registration of a class-based handler. Annotation processors from the
|
||||
* parent app and any installed on this scope are applied. The {@link Route @Route}
|
||||
* path is prepended with this scope's namespace automatically.
|
||||
*
|
||||
* <p>Calling {@link RouteHandle#with} is optional — the route is registered
|
||||
* automatically before the next operation or when the scope consumer returns.
|
||||
* Registers a class-based handler annotated with {@link Route @Route}.
|
||||
* Not part of {@link FlashRegistrar} — scope-only convenience.
|
||||
*/
|
||||
@Override
|
||||
public RouteHandle<FlashScope> register(RequestHandler handler) {
|
||||
return track(new RouteHandle<>(this, explicit -> {
|
||||
List<Middleware> injected = ctx.processors().stream()
|
||||
.flatMap(p -> p.process(handler.getClass()).stream())
|
||||
.toList();
|
||||
Middleware[] all = injected.isEmpty()
|
||||
? explicit
|
||||
: Stream.concat(injected.stream(), Arrays.stream(explicit)).toArray(Middleware[]::new);
|
||||
Route annotation = handler.getClass().getAnnotation(Route.class);
|
||||
if (annotation != null) {
|
||||
String full = ns(annotation.path());
|
||||
emit(annotation.method(), full, handler.getClass(), injected, explicit);
|
||||
router.doRegister(annotation.method(), full, (RequestHandler) handler, all);
|
||||
}
|
||||
}));
|
||||
RouteHandle<FlashScope> register(RequestHandler handler) {
|
||||
Route ann = handler.getClass().getAnnotation(Route.class);
|
||||
if (ann == null)
|
||||
throw new InitializationException(
|
||||
handler.getClass().getName() + " is missing @Route");
|
||||
String full = ns(ann.path());
|
||||
Middleware[] scopeMw = snapshotScopeMiddlewares();
|
||||
return track(new RouteHandle<>(this, mw ->
|
||||
deferredRoutes.add(new RouteDefinition(ann.method(), full, handler, scopeMw, mw, true, ctx, namespace))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans {@code packageName} for {@link RequestHandler} subclasses annotated with
|
||||
* {@link Route @Route}. Each is instantiated via its no-arg constructor and registered
|
||||
* with this scope's namespace prefix and annotation processors applied.
|
||||
*/
|
||||
@Override
|
||||
public FlashScope scan(String packageName) {
|
||||
flushPending();
|
||||
PackageScanner.findHandlers(packageName).forEach(cls -> register(instantiate(cls)).ensureRegistered());
|
||||
PackageScanner.findHandlers(packageName).forEach(cls ->
|
||||
register(instantiate(cls)).ensureRegistered());
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlashScope onException(AbstractRouter.ExceptionHandler h) {
|
||||
flushPending();
|
||||
router.onException(h);
|
||||
return this;
|
||||
}
|
||||
public FlashContext ctx() { return ctx; }
|
||||
|
||||
@Override
|
||||
public FlashScope onNotFound(SimpleHandler.FunctionalHandler h) {
|
||||
flushPending();
|
||||
router.onNotFound(h);
|
||||
return this;
|
||||
}
|
||||
// ── Internals (called by FlashApp.mount) ──────────────────────────────────
|
||||
|
||||
@Override
|
||||
public ExtensionContext ctx() {
|
||||
return ctx;
|
||||
}
|
||||
void flush() { flushPending(); }
|
||||
|
||||
// ── Internals ─────────────────────────────────────────────────────────────
|
||||
List<RouteDefinition> routes() { return deferredRoutes; }
|
||||
|
||||
/** Ensures any pending route is registered when the scope consumer returns. */
|
||||
void flush() {
|
||||
flushPending();
|
||||
}
|
||||
|
||||
/** Returns the sub-router for GlobalRouter to mount. */
|
||||
AbstractRouter router() {
|
||||
return router;
|
||||
}
|
||||
|
||||
/** Prepends this scope's namespace to the given path. */
|
||||
private String ns(String path) {
|
||||
return namespace + PathUtils.sanitize(path);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Middleware[] snapshotScopeMiddlewares() {
|
||||
return scopeMiddlewares.isEmpty() ? NO_MW : scopeMiddlewares.toArray(NO_MW);
|
||||
}
|
||||
|
||||
private static RequestHandler instantiate(Class<?> cls) {
|
||||
try {
|
||||
return (RequestHandler) cls.getDeclaredConstructor().newInstance();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to instantiate handler: " + cls.getName() +
|
||||
throw new InitializationException(
|
||||
"Failed to instantiate " + cls.getName() +
|
||||
" — ensure it has a public no-arg constructor", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits a {@link RouteEvent} to all {@link RouteListener}s visible from this scope's context.
|
||||
* Parent-level listeners (registered on the app) are included via context inheritance.
|
||||
* No-op if no listener has been registered. Never called on the request hot-path.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void emit(HttpMethod method, String path, Class<?> handlerClass,
|
||||
List<Middleware> injected, Middleware[] explicit) {
|
||||
List<RouteListener> listeners = ctx.routeListeners();
|
||||
if (listeners.isEmpty()) return;
|
||||
Middleware[] routerMws = router.routerMiddlewares();
|
||||
List<Class<? extends Middleware>> chain = new ArrayList<>(routerMws.length + injected.size() + explicit.length);
|
||||
for (Middleware m : routerMws) chain.add((Class<? extends Middleware>) m.getClass());
|
||||
for (Middleware m : injected) chain.add((Class<? extends Middleware>) m.getClass());
|
||||
for (Middleware m : explicit) chain.add((Class<? extends Middleware>) m.getClass());
|
||||
RouteEvent event = new RouteEvent(method, path, namespace,
|
||||
router.getClass().getSimpleName(), handlerClass, List.copyOf(chain));
|
||||
listeners.forEach(l -> l.onRoute(event));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.relism.extension;
|
||||
|
||||
import dev.relism.exceptions.InitializationException;
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.routing.Route;
|
||||
|
||||
@@ -15,6 +16,10 @@ import java.util.jar.JarFile;
|
||||
* Minimal classpath scanner used by {@link FlashApp#scan} and {@link FlashScope#scan}.
|
||||
* Finds all classes in a package that extend {@link RequestHandler} and carry {@link Route @Route}.
|
||||
* Supports both exploded directories (development) and fat JARs (deployment).
|
||||
*
|
||||
* <p><b>Fail-fast:</b> if the package does not exist, contains no handlers, or a handler
|
||||
* class cannot be loaded, an {@link InitializationException} is thrown immediately.
|
||||
* A clear crash at boot is always preferable to a server that starts "empty".
|
||||
*/
|
||||
final class PackageScanner {
|
||||
|
||||
@@ -22,55 +27,84 @@ final class PackageScanner {
|
||||
|
||||
/**
|
||||
* Returns all {@link RequestHandler} subclasses in {@code packageName} that carry
|
||||
* {@link Route @Route} and have a public no-arg constructor.
|
||||
* {@link Route @Route}.
|
||||
*
|
||||
* @throws InitializationException if the package is empty, does not exist, or a
|
||||
* handler class fails to load
|
||||
*/
|
||||
static List<Class<?>> findHandlers(String packageName) {
|
||||
if (packageName == null || packageName.isBlank())
|
||||
throw new InitializationException("scan() called with null or blank package name");
|
||||
|
||||
String resourcePath = packageName.replace('.', '/');
|
||||
ClassLoader cl = Thread.currentThread().getContextClassLoader();
|
||||
List<Class<?>> result = new ArrayList<>();
|
||||
List<String> errors = new ArrayList<>();
|
||||
boolean packageFound = false;
|
||||
|
||||
try {
|
||||
Enumeration<URL> resources = cl.getResources(resourcePath);
|
||||
while (resources.hasMoreElements()) {
|
||||
packageFound = true;
|
||||
URL url = resources.nextElement();
|
||||
String protocol = url.getProtocol();
|
||||
if ("file".equals(protocol)) {
|
||||
scanDirectory(new File(url.toURI()), packageName, cl, result);
|
||||
scanDirectory(new File(url.toURI()), packageName, cl, result, errors);
|
||||
} else if ("jar".equals(protocol)) {
|
||||
String jarPath = url.getPath();
|
||||
// jar:file:/path/to/app.jar!/com/example → /path/to/app.jar
|
||||
String filePart = jarPath.substring(jarPath.indexOf("file:") + 5, jarPath.indexOf('!'));
|
||||
try (JarFile jar = new JarFile(filePart)) {
|
||||
scanJar(jar, resourcePath, packageName, cl, result);
|
||||
scanJar(jar, resourcePath, packageName, cl, result, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (InitializationException e) {
|
||||
throw e; // re-throw our own exceptions
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to scan package: " + packageName, e);
|
||||
throw new InitializationException("Failed to scan package: " + packageName, e);
|
||||
}
|
||||
|
||||
if (!packageFound)
|
||||
throw new InitializationException(
|
||||
"scan(\"" + packageName + "\") — package not found on classpath. " +
|
||||
"Verify the package name and ensure the module is on the classpath.");
|
||||
|
||||
if (!errors.isEmpty())
|
||||
throw new InitializationException(
|
||||
"scan(\"" + packageName + "\") — failed to load " + errors.size() + " handler(s):\n • " +
|
||||
String.join("\n • ", errors));
|
||||
|
||||
if (result.isEmpty())
|
||||
throw new InitializationException(
|
||||
"scan(\"" + packageName + "\") — no @Route handlers found. " +
|
||||
"Ensure handler classes extend RequestHandler, carry @Route, are not abstract, " +
|
||||
"and have a public no-arg constructor.");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void scanDirectory(File dir, String packageName, ClassLoader cl, List<Class<?>> result) {
|
||||
private static void scanDirectory(File dir, String packageName, ClassLoader cl,
|
||||
List<Class<?>> result, List<String> errors) {
|
||||
File[] files = dir.listFiles();
|
||||
if (files == null) return;
|
||||
for (File file : files) {
|
||||
if (file.isDirectory()) {
|
||||
scanDirectory(file, packageName + '.' + file.getName(), cl, result);
|
||||
scanDirectory(file, packageName + '.' + file.getName(), cl, result, errors);
|
||||
} else if (file.getName().endsWith(".class") && !isAnonymous(file.getName())) {
|
||||
String className = packageName + '.' + file.getName().replace(".class", "");
|
||||
tryLoad(className, cl, result);
|
||||
tryLoad(className, cl, result, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void scanJar(JarFile jar, String resourcePath, String packageName,
|
||||
ClassLoader cl, List<Class<?>> result) {
|
||||
ClassLoader cl, List<Class<?>> result, List<String> errors) {
|
||||
Enumeration<JarEntry> entries = jar.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
String name = entries.nextElement().getName();
|
||||
if (name.startsWith(resourcePath) && name.endsWith(".class") && !isAnonymous(name)) {
|
||||
String className = name.replace('/', '.').replace(".class", "");
|
||||
tryLoad(className, cl, result);
|
||||
tryLoad(className, cl, result, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,23 +118,34 @@ final class PackageScanner {
|
||||
private static boolean isAnonymous(String fileName) {
|
||||
int dollar = fileName.lastIndexOf('$');
|
||||
if (dollar < 0) return false;
|
||||
// skip past any extra '$' (lambda desugaring may produce '$$Lambda$...')
|
||||
int next = dollar + 1;
|
||||
while (next < fileName.length() && fileName.charAt(next) == '$') next++;
|
||||
return next < fileName.length() && Character.isDigit(fileName.charAt(next));
|
||||
}
|
||||
|
||||
private static void tryLoad(String className, ClassLoader cl, List<Class<?>> result) {
|
||||
private static void tryLoad(String className, ClassLoader cl,
|
||||
List<Class<?>> result, List<String> errors) {
|
||||
try {
|
||||
Class<?> cls = cl.loadClass(className);
|
||||
if (RequestHandler.class.isAssignableFrom(cls)
|
||||
&& cls.isAnnotationPresent(Route.class)
|
||||
&& !java.lang.reflect.Modifier.isAbstract(cls.getModifiers())) {
|
||||
cls.getDeclaredConstructor(); // verify no-arg constructor exists
|
||||
result.add(cls);
|
||||
if (!RequestHandler.class.isAssignableFrom(cls)) return;
|
||||
if (java.lang.reflect.Modifier.isAbstract(cls.getModifiers())) return;
|
||||
if (!cls.isAnnotationPresent(Route.class)) return;
|
||||
|
||||
// Verify no-arg constructor exists — fail-fast if missing
|
||||
try {
|
||||
cls.getDeclaredConstructor();
|
||||
} catch (NoSuchMethodException e) {
|
||||
errors.add(className + " — missing public no-arg constructor");
|
||||
return;
|
||||
}
|
||||
} catch (Exception | Error ignored) {
|
||||
// Skip classes that cannot be loaded or don't meet criteria
|
||||
|
||||
result.add(cls);
|
||||
} catch (ClassNotFoundException e) {
|
||||
errors.add(className + " — class not found: " + e.getMessage());
|
||||
} catch (NoClassDefFoundError e) {
|
||||
errors.add(className + " — missing dependency: " + e.getMessage());
|
||||
} catch (LinkageError e) {
|
||||
errors.add(className + " — linkage error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package dev.relism.extension;
|
||||
|
||||
import dev.relism.http.HttpMethod;
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.routing.Middleware;
|
||||
|
||||
/**
|
||||
* Immutable snapshot of a route captured during the builder phase.
|
||||
*
|
||||
* <p>Accumulated by {@link FlashApp} and {@link FlashScope}. All definitions —
|
||||
* regardless of origin — are compiled into a single flat router at
|
||||
* {@link FlashApp#start()}.
|
||||
*
|
||||
* @param method HTTP method
|
||||
* @param path fully resolved path (namespace already prepended for scope routes)
|
||||
* @param handler SimpleHandler for lambdas, user handler for class-based
|
||||
* @param scopeMiddlewares middlewares from {@link FlashScope#use} (empty for app-level routes)
|
||||
* @param explicitMiddlewares middlewares from {@link dev.relism.routing.RouteHandle#with}
|
||||
* @param classBasedHandler true → needs annotation processing + context binding
|
||||
* @param ctx the FlashContext for this route's binding and processors
|
||||
* @param namespace logical namespace for route events ("/" for app, "/api" for scope, etc.)
|
||||
*/
|
||||
record RouteDefinition(
|
||||
HttpMethod method,
|
||||
String path,
|
||||
RequestHandler handler,
|
||||
Middleware[] scopeMiddlewares,
|
||||
Middleware[] explicitMiddlewares,
|
||||
boolean classBasedHandler,
|
||||
FlashContext ctx,
|
||||
String namespace
|
||||
) {}
|
||||
@@ -3,7 +3,7 @@ package dev.relism.extension;
|
||||
/**
|
||||
* Observer notified once for each route registered on a {@link FlashApp} or {@link FlashScope}.
|
||||
*
|
||||
* <p>Register via {@link ExtensionContext#addRouteListener}. The listener is called
|
||||
* <p>Register via {@link FlashContext#addRouteListener}. The listener is called
|
||||
* <em>once per route at boot time</em>, before the route is handed to the routing engine.
|
||||
* There is zero overhead on the request hot-path.
|
||||
*
|
||||
|
||||
@@ -10,6 +10,7 @@ import lombok.Value;
|
||||
import lombok.experimental.NonFinal;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
@@ -43,24 +44,42 @@ public class Request {
|
||||
@NonFinal @Setter PathParams pathParams;
|
||||
@NonFinal @Setter QueryParams queryParams;
|
||||
|
||||
private Request(RequestLine requestLine, RequestBody body) {
|
||||
this.requestLine = requestLine;
|
||||
this.body = body;
|
||||
this.pathParams = null;
|
||||
this.queryParams = null;
|
||||
/**
|
||||
* Remote socket address of the connected client. Set once at connection time from
|
||||
* {@link java.net.Socket#getRemoteSocketAddress()} — the {@link InetSocketAddress}
|
||||
* object already exists in the JDK and is passed by reference: zero allocation,
|
||||
* zero copy. {@code null} only in test-constructed requests.
|
||||
*
|
||||
* <p>Use {@link #remoteAddress()} to access it. String conversion
|
||||
* ({@code .getAddress().getHostAddress()}) is deferred to the caller — lazy and
|
||||
* only paid when actually needed.
|
||||
*/
|
||||
@Getter(lombok.AccessLevel.NONE)
|
||||
@EqualsAndHashCode.Exclude
|
||||
@ToString.Exclude
|
||||
InetSocketAddress remoteAddress;
|
||||
|
||||
private Request(RequestLine requestLine, RequestBody body, InetSocketAddress remoteAddress) {
|
||||
this.requestLine = requestLine;
|
||||
this.body = body;
|
||||
this.pathParams = null;
|
||||
this.queryParams = null;
|
||||
this.remoteAddress = remoteAddress;
|
||||
}
|
||||
|
||||
/** Test / manual constructor — {@code remoteAddress()} returns {@code null}. */
|
||||
public Request(RequestLine requestLine, byte[] body) {
|
||||
this(requestLine, RequestBody.of(body));
|
||||
this(requestLine, RequestBody.of(body), null);
|
||||
}
|
||||
|
||||
public static Request forParsed(RequestLine requestLine, InputStream stream,
|
||||
long contentLength, byte[] headerBuf,
|
||||
int bodyStart, int preBufLen) {
|
||||
int bodyStart, int preBufLen,
|
||||
InetSocketAddress remoteAddress) {
|
||||
RequestBody rb = contentLength > 0 ? new RequestBody(stream, contentLength, headerBuf, bodyStart, preBufLen)
|
||||
: contentLength == 0 ? RequestBody.empty()
|
||||
: /* chunked */ new RequestBody(stream, -1L, null, 0, 0);
|
||||
return new Request(requestLine, rb);
|
||||
return new Request(requestLine, rb, remoteAddress);
|
||||
}
|
||||
|
||||
// ── Request line ──────────────────────────────────────────────────────────
|
||||
@@ -126,6 +145,22 @@ public class Request {
|
||||
*/
|
||||
public List<String> queries(String name) { return resolveQueryParams().getAll(name); }
|
||||
|
||||
// ── Remote address ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Returns the remote socket address of the connected client, or {@code null}
|
||||
* for test-constructed requests.
|
||||
*
|
||||
* <p>The {@link InetSocketAddress} is the JDK object created during
|
||||
* {@link java.net.ServerSocket#accept()} — no allocation occurs here.
|
||||
* To obtain the IP string (lazy, allocates once):
|
||||
* <pre>{@code
|
||||
* InetSocketAddress addr = req.remoteAddress();
|
||||
* if (addr != null) String ip = addr.getAddress().getHostAddress();
|
||||
* }</pre>
|
||||
*/
|
||||
public InetSocketAddress remoteAddress() { return remoteAddress; }
|
||||
|
||||
// ── Body ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +1,134 @@
|
||||
package dev.relism.models;
|
||||
|
||||
import dev.relism.extension.FlashContext;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Base class for class-based route handlers.
|
||||
*
|
||||
* <p>Annotate the subclass with {@link dev.relism.routing.Route @Route} and register it
|
||||
* via {@link dev.relism.routing.AbstractRouter#register}. For one-off routes, prefer the
|
||||
* lambda DSL ({@code server.get(path, handler)}) which wraps a {@link SimpleHandler} internally.
|
||||
* via {@link dev.relism.extension.FlashApp#register} or {@link dev.relism.extension.FlashApp#scan}.
|
||||
* For one-off routes, prefer the lambda DSL ({@code app.get(path, handler)}).
|
||||
*
|
||||
* <h3>Lifecycle</h3>
|
||||
* <ol>
|
||||
* <li>Instantiation — no-arg constructor (for scan) or manual {@code new Handler(...)}</li>
|
||||
* <li>{@link #bind} — called once by the framework at {@code start()}, injects the
|
||||
* {@link FlashContext} and invokes {@link #onInit()}</li>
|
||||
* <li>{@link #handle} — called on every matching request (hot-path, zero-alloc)</li>
|
||||
* </ol>
|
||||
*
|
||||
* <h3>Service access</h3>
|
||||
* Override {@link #onInit()} to cache services from the {@link FlashContext}
|
||||
* into private fields. This keeps the hot-path ({@code handle}) free of map lookups.
|
||||
*
|
||||
* <pre>{@code
|
||||
* @Route(method = HttpMethod.GET, path = "/users")
|
||||
* public class UserHandler extends RequestHandler {
|
||||
* private UserService users;
|
||||
*
|
||||
* @Override protected void onInit() {
|
||||
* users = require(UserService.class);
|
||||
* }
|
||||
*
|
||||
* @Override public Object handle(Request req, Response res) {
|
||||
* return users.findAll();
|
||||
* }
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
public abstract class RequestHandler {
|
||||
|
||||
private FlashContext ctx;
|
||||
|
||||
/**
|
||||
* Called once by the framework after instantiation, before the first request.
|
||||
* Injects the {@link FlashContext} and triggers {@link #onInit()}.
|
||||
*
|
||||
* <p><b>Infrastructure method</b> — do not call from user code.
|
||||
* Use {@link dev.relism.extension.FlashApp#register} or
|
||||
* {@link dev.relism.extension.FlashApp#scan} instead.
|
||||
*/
|
||||
public final void bind(FlashContext ctx) {
|
||||
this.ctx = ctx;
|
||||
onInit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to cache services at boot time. Called once after {@link #bind},
|
||||
* before any request reaches this handler.
|
||||
*
|
||||
* <p>Use {@link #require} and {@link #find} to retrieve services from the
|
||||
* {@link FlashContext}. Cache them in private fields so the hot-path
|
||||
* ({@link #handle}) has zero lookup overhead.
|
||||
*
|
||||
* <p><b>Important:</b> if your class extends another handler base (e.g.
|
||||
* {@code JacksonHandler}), call {@code super.onInit()} first so the parent
|
||||
* can initialise its own services.
|
||||
*
|
||||
* <pre>{@code
|
||||
* @Override protected void onInit() {
|
||||
* super.onInit();
|
||||
* myService = require(MyService.class);
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
protected void onInit() {}
|
||||
|
||||
/**
|
||||
* Retrieves a required service from the {@link FlashContext}.
|
||||
* Throws {@link IllegalStateException} if the service is not registered or
|
||||
* this handler has not been bound yet.
|
||||
*
|
||||
* <p>Typically called inside {@link #onInit()} to cache the result.
|
||||
*
|
||||
* @param type the service class
|
||||
* @param <T> the service type
|
||||
* @return the service instance, never null
|
||||
*/
|
||||
protected <T> T require(Class<T> type) {
|
||||
checkBound();
|
||||
return ctx.require(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up an optional service from the {@link FlashContext}.
|
||||
*
|
||||
* @param type the service class
|
||||
* @param <T> the service type
|
||||
* @return the service, or empty if not registered
|
||||
*/
|
||||
protected <T> Optional<T> find(Class<T> type) {
|
||||
checkBound();
|
||||
return ctx.find(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up an optional service from the {@link FlashContext}.
|
||||
* Identical to {@link #find} — prefer this name for expressive call sites
|
||||
* ({@code optional(ViewEngine.class).ifPresent(...)}).
|
||||
*
|
||||
* @param type the service class
|
||||
* @param <T> the service type
|
||||
* @return the service, or empty if not registered
|
||||
*/
|
||||
protected <T> Optional<T> optional(Class<T> type) {
|
||||
checkBound();
|
||||
return ctx.optional(type);
|
||||
}
|
||||
|
||||
private void checkBound() {
|
||||
if (ctx == null)
|
||||
throw new IllegalStateException(
|
||||
getClass().getSimpleName() + " has not been bound to a FlashContext — " +
|
||||
"register via FlashApp.register() or FlashApp.scan(), not directly on the router");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an incoming request. The return value determines the response body:
|
||||
* return a {@link Response} to replace the whole response, any other non-null value
|
||||
* to set it as the body, or {@code null} to leave the response as-is.
|
||||
*/
|
||||
public abstract Object handle(Request request, Response response) throws Exception;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,24 @@
|
||||
package dev.relism.routing;
|
||||
|
||||
import dev.relism.fpr.core.ByteView;
|
||||
import dev.relism.http.ContentType;
|
||||
import dev.relism.http.HttpMethod;
|
||||
import dev.relism.fpr.core.ByteView;
|
||||
import dev.relism.models.*;
|
||||
import dev.relism.template.ErrorPages;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Base router. Each router has a namespace prefix (default {@code "/"}).
|
||||
* Error handlers are scoped to this router.
|
||||
* Base router contract. Owns error handlers and the compile-time middleware
|
||||
* wrapping logic. The only concrete implementation is
|
||||
* {@link dev.relism.routing.routers.fastpathrouter.FastPathRouterImpl}.
|
||||
*
|
||||
* <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 {@link #doRegister} 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 overhead.
|
||||
*
|
||||
* <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.
|
||||
*
|
||||
* <p><b>Registration</b>: use {@link dev.relism.extension.FlashApp} or
|
||||
* {@link dev.relism.extension.FlashScope} — they are the public registration API.
|
||||
* {@link #doRegister} is an infrastructure method for those entry points.
|
||||
* <p><b>Registration</b>: use {@link dev.relism.extension.FlashApp} — the single
|
||||
* public registration API. {@link #doRegister} is an infrastructure method.
|
||||
*/
|
||||
public abstract class AbstractRouter {
|
||||
|
||||
@Getter
|
||||
protected String namespace = "/";
|
||||
|
||||
@Getter(AccessLevel.PACKAGE)
|
||||
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;
|
||||
|
||||
/**
|
||||
* Raw router-level middleware array, kept for route event emission by
|
||||
* {@link dev.relism.extension.FlashApp} and {@link dev.relism.extension.FlashScope}.
|
||||
* Never mutated after construction. Not used on the hot-path.
|
||||
*/
|
||||
private final Middleware[] rawRouterMiddlewares;
|
||||
|
||||
protected SimpleHandler notFoundHandler = new SimpleHandler((req, res) -> {
|
||||
res.setStatusCode(404);
|
||||
res.setContentType(ContentType.TEXT_HTML);
|
||||
@@ -60,113 +31,44 @@ public abstract class AbstractRouter {
|
||||
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.rawRouterMiddlewares = middlewares;
|
||||
this.routerMiddleware = middlewares.length == 0 ? null
|
||||
: middlewares.length == 1 ? middlewares[0]
|
||||
: Middleware.of(middlewares);
|
||||
}
|
||||
public SimpleHandler getNotFoundHandler() { return notFoundHandler; }
|
||||
public ExceptionHandler getExceptionHandler() { return exceptionHandler; }
|
||||
|
||||
/**
|
||||
* Returns the raw router-level middleware array as passed at construction.
|
||||
* Used by {@link dev.relism.extension.FlashApp} and {@link dev.relism.extension.FlashScope}
|
||||
* to populate {@link dev.relism.extension.RouteEvent#middlewareChain()}.
|
||||
* Never mutated; never called on the hot-path.
|
||||
*/
|
||||
public Middleware[] routerMiddlewares() { return rawRouterMiddlewares; }
|
||||
|
||||
// ── Internal wiring ───────────────────────────────────────────────────────
|
||||
|
||||
SimpleHandler getNotFoundHandler() { return notFoundHandler; }
|
||||
ExceptionHandler getExceptionHandler() { return exceptionHandler; }
|
||||
|
||||
void setNamespace(String namespace) {
|
||||
this.namespace = namespace;
|
||||
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;
|
||||
for (int i = handlerMiddlewares.length - 1; i >= 0; i--)
|
||||
compiled = new SimpleHandler(handlerMiddlewares[i].wrap(compiled));
|
||||
if (routerMiddleware != null)
|
||||
compiled = new SimpleHandler(routerMiddleware.wrap(compiled));
|
||||
return compiled;
|
||||
}
|
||||
|
||||
// ── Error handler configuration ───────────────────────────────────────────
|
||||
|
||||
public AbstractRouter onNotFound(SimpleHandler.FunctionalHandler handler) {
|
||||
public void onNotFound(SimpleHandler.FunctionalHandler handler) {
|
||||
this.notFoundHandler = new SimpleHandler(handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
public AbstractRouter onException(ExceptionHandler handler) {
|
||||
public void onException(ExceptionHandler handler) {
|
||||
this.exceptionHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
// ── Infrastructure registration ───────────────────────────────────────────
|
||||
// Used by FlashApp and FlashScope. Not part of the public user-facing API.
|
||||
// ── Infrastructure registration ──────────────────────────────────────────
|
||||
// Used by FlashApp at compile time. Not part of the user-facing API.
|
||||
|
||||
/**
|
||||
* Registers a lambda handler immediately with a pre-built middleware array.
|
||||
* Infrastructure method — use {@link dev.relism.extension.FlashApp} instead.
|
||||
*/
|
||||
public AbstractRouter doRegister(HttpMethod method, String path,
|
||||
SimpleHandler.FunctionalHandler handler, Middleware[] middlewares) {
|
||||
return addRoute(method, PathUtils.sanitize(path),
|
||||
compile(new SimpleHandler(handler), middlewares));
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a class-based handler immediately with a pre-built middleware array.
|
||||
* Reads {@link Route @Route} for method and path.
|
||||
* Infrastructure method — use {@link dev.relism.extension.FlashApp} instead.
|
||||
*/
|
||||
public AbstractRouter doRegister(RequestHandler handler, Middleware[] middlewares) {
|
||||
Route annotation = handler.getClass().getAnnotation(Route.class);
|
||||
if (annotation != null)
|
||||
addRoute(annotation.method(), annotation.path(),
|
||||
compile(handler, middlewares));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a class-based handler with an explicit method and path (ignoring the
|
||||
* {@link Route @Route} annotation's path). Used by {@link dev.relism.extension.FlashScope}
|
||||
* to prepend the scope's namespace prefix.
|
||||
* Infrastructure method — use {@link dev.relism.extension.FlashScope} instead.
|
||||
* Registers a handler with a pre-built middleware array.
|
||||
* Compiles the middleware chain once at boot — zero overhead on hot-path.
|
||||
*/
|
||||
public AbstractRouter doRegister(HttpMethod method, String path,
|
||||
RequestHandler handler, Middleware[] middlewares) {
|
||||
return addRoute(method, PathUtils.sanitize(path), compile(handler, middlewares));
|
||||
}
|
||||
|
||||
// ── Routing ───────────────────────────────────────────────────────────────
|
||||
/**
|
||||
* Compiles handler-level middleware into a single wrapped handler.
|
||||
* Called once per route at registration time.
|
||||
*/
|
||||
private RequestHandler compile(RequestHandler handler, Middleware[] middlewares) {
|
||||
RequestHandler compiled = handler;
|
||||
for (int i = middlewares.length - 1; i >= 0; i--)
|
||||
compiled = new SimpleHandler(middlewares[i].wrap(compiled));
|
||||
return compiled;
|
||||
}
|
||||
|
||||
// ── Routing ──────────────────────────────────────────────────────────────
|
||||
|
||||
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 static void setPathParams(Request request, String[] names, ByteView source, int[] starts, int[] lens) {
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
package dev.relism.routing;
|
||||
|
||||
import dev.relism.exceptions.DuplicateNamespaceException;
|
||||
import dev.relism.fpr.core.ByteView;
|
||||
import dev.relism.http.HttpMethod;
|
||||
import dev.relism.models.Request;
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.routing.routers.fastpathrouter.FastPathRouterImpl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Top-level dispatcher. Routes to the longest-matching mounted sub-router first,
|
||||
* falling back to the internal {@link FastPathRouterImpl}.
|
||||
*/
|
||||
public class GlobalRouter extends AbstractRouter {
|
||||
private final Map<String, AbstractRouter> subRoutersMap = new HashMap<>();
|
||||
private final List<AbstractRouter> sortedSubRouters = new ArrayList<>();
|
||||
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) {
|
||||
String sanitized = PathUtils.sanitize(namespace);
|
||||
if (subRoutersMap.containsKey(sanitized)) throw new DuplicateNamespaceException(sanitized);
|
||||
|
||||
router.setNamespace(sanitized);
|
||||
subRoutersMap.put(sanitized, router);
|
||||
sortedSubRouters.add(router);
|
||||
sortedSubRouters.sort(Comparator.comparingInt((AbstractRouter r) -> r.getNamespaceBytes().length).reversed());
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestHandler route(Request request) {
|
||||
ByteView path = request.getRequestLine().getPath();
|
||||
for (AbstractRouter sub : sortedSubRouters) {
|
||||
if (startsWith(path, sub.getNamespaceBytes())) {
|
||||
RequestHandler h = sub.route(request);
|
||||
return h != null ? h : sub.getNotFoundHandler();
|
||||
}
|
||||
}
|
||||
RequestHandler h = internalRouter.route(request);
|
||||
return h != null ? h : notFoundHandler;
|
||||
}
|
||||
|
||||
public ExceptionHandler resolveExceptionHandler(Request request) {
|
||||
ByteView path = request.getRequestLine().getPath();
|
||||
for (AbstractRouter sub : sortedSubRouters) {
|
||||
if (startsWith(path, sub.getNamespaceBytes())) return sub.getExceptionHandler();
|
||||
}
|
||||
return exceptionHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler) {
|
||||
return internalRouter.addRoute(method, PathUtils.sanitize(path), handler);
|
||||
}
|
||||
|
||||
private static boolean startsWith(ByteView view, byte[] prefix) {
|
||||
if (view.length() < prefix.length) return false;
|
||||
for (int i = 0; i < prefix.length; i++) {
|
||||
if (view.byteAt(i) != prefix[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+2
-6
@@ -9,8 +9,6 @@ import dev.relism.http.HttpMethod;
|
||||
import dev.relism.models.Request;
|
||||
import dev.relism.models.RequestHandler;
|
||||
import dev.relism.routing.AbstractRouter;
|
||||
import dev.relism.routing.Middleware;
|
||||
import dev.relism.routing.PathUtils;
|
||||
|
||||
/**
|
||||
* Router backed by the {@code fpr-core} byte-level state machine. Routes are compiled lazily
|
||||
@@ -23,8 +21,7 @@ public class FastPathRouterImpl extends AbstractRouter {
|
||||
private volatile FastPathRouter<ByteView, RequestHandler> router;
|
||||
private String[] cachedParamNames;
|
||||
|
||||
public FastPathRouterImpl(Middleware... middlewares) { super(middlewares); }
|
||||
public FastPathRouterImpl() { super(); }
|
||||
public FastPathRouterImpl() {}
|
||||
|
||||
private static final class FastPathRouterContext {
|
||||
private static final ThreadLocal<MatchResult<RequestHandler>> RESULT_HOLDER =
|
||||
@@ -43,8 +40,7 @@ public class FastPathRouterImpl extends AbstractRouter {
|
||||
|
||||
@Override
|
||||
protected AbstractRouter addRoute(HttpMethod method, String path, RequestHandler handler) {
|
||||
String fullPath = PathUtils.join(namespace, path);
|
||||
builder.add(StringRouteParser.parse(method.name() + fullPath), handler);
|
||||
builder.add(StringRouteParser.parse(method.name() + path), handler);
|
||||
this.router = null;
|
||||
return this;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user