package dev.coph.nextusweb.server.annotation; import dev.coph.nextusweb.server.router.Request; import dev.coph.nextusweb.server.router.Response; import dev.coph.nextusweb.server.router.Router; import io.netty.handler.codec.http.HttpMethod; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.lang.reflect.Method; /** * Reflective registrar that wires the routing annotations on a controller object into a * {@link Router}. * *

Given a controller instance, the scanner reads the optional {@link Controller} annotation * to determine a path prefix, then walks every declared method looking for one of the * supported route annotations ({@link Route}, {@link GET}, {@link POST}, {@link PUT}, * {@link DELETE}, {@link PATCH} or {@link CUSTOM}). For each matching method it:

*
    *
  1. validates that the method has the required {@code (Request, Response)} signature and * a {@code void} return type;
  2. *
  3. creates a {@link MethodHandle} bound to the controller instance for fast, * reflection-free invocation;
  4. *
  5. registers a {@link Router.Handler} that delegates to that handle under the resolved * HTTP method and full path.
  6. *
* *

This class is a stateless utility and cannot be instantiated.

* * @see Controller * @see Router */ public final class AnnotationScanner { /** * Shared lookup used to unreflect controller methods into {@link MethodHandle}s. A single * lookup is sufficient because the scanner forces accessibility on each method before * unreflecting it. */ private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); /** * The exact method type every handler must conform to: {@code void (Request, Response)}. * Used as documentation of the contract enforced by {@link #validateSignature(Method)}. */ private static final MethodType HANDLER_TYPE = MethodType.methodType(void.class, Request.class, Response.class); /** * Private constructor preventing instantiation of this stateless utility class. */ private AnnotationScanner() { } /** * Scans the given controller for route annotations and registers every discovered handler * with the supplied router. * *

If the controller class is annotated with {@link Controller}, its value is used as a * path prefix for all routes. Methods without a recognised route annotation are ignored. * A line describing each registered route is printed to standard output.

* * @param router the router to register the discovered handlers with * @param controller the controller instance whose annotated methods should be registered * @throws IllegalArgumentException if an annotated method has an invalid signature * @throws RuntimeException if a method cannot be made accessible or unreflected */ public static void register(Router router, Object controller) { Class clazz = controller.getClass(); Controller ctrlAnno = clazz.getAnnotation(Controller.class); String prefix = ""; if (ctrlAnno != null) { prefix = normalizePrefix(ctrlAnno.value()); } for (Method method : clazz.getDeclaredMethods()) { RouteInfo info = extractRoute(method); if (info == null) continue; validateSignature(method); try { method.setAccessible(true); MethodHandle mh = LOOKUP.unreflect(method).bindTo(controller); Router.Handler handler = (req, res) -> { try { mh.invokeExact(req, res); } catch (Throwable t) { if (t instanceof Exception e) throw e; throw new RuntimeException(t); } }; String fullPath = prefix + normalizePath(info.path()); router.register(HttpMethod.valueOf(info.method()), fullPath, handler); System.out.printf("Registered: %-6s %-30s -> %s.%s%n", info.method(), fullPath, clazz.getSimpleName(), method.getName()); } catch (IllegalAccessException e) { throw new RuntimeException("Could not register method: " + method, e); } } } /** * Normalizes a controller-level path prefix by ensuring it starts with a single leading * slash. * * @param p the raw prefix from the {@link Controller} annotation, may be {@code null} or empty * @return the normalized prefix, or an empty string if {@code p} is {@code null} or empty */ private static String normalizePrefix(String p) { if (p == null || p.isEmpty()) return ""; return p.startsWith("/") ? p : "/" + p; } /** * Extracts route metadata (HTTP method and path) from a method by inspecting the supported * route annotations in priority order. {@link Route} is checked first, followed by the * verb-specific annotations and finally {@link CUSTOM}. * * @param m the method to inspect * @return a {@link RouteInfo} describing the route, or {@code null} if the method carries * no recognised route annotation */ private static RouteInfo extractRoute(Method m) { Route r = m.getAnnotation(Route.class); if (r != null) return new RouteInfo(r.method(), r.path()); GET get = m.getAnnotation(GET.class); if (get != null) return new RouteInfo("GET", get.value()); POST post = m.getAnnotation(POST.class); if (post != null) return new RouteInfo("POST", post.value()); PUT put = m.getAnnotation(PUT.class); if (put != null) return new RouteInfo("PUT", put.value()); DELETE del = m.getAnnotation(DELETE.class); if (del != null) return new RouteInfo("DELETE", del.value()); PATCH patch = m.getAnnotation(PATCH.class); if (patch != null) return new RouteInfo("PATCH", patch.value()); CUSTOM custom = m.getAnnotation(CUSTOM.class); if (custom != null) return new RouteInfo(custom.method(), custom.value()); return null; } /** * Validates that a handler method conforms to the required {@code void (Request, Response)} * contract. * * @param m the method to validate * @throws IllegalArgumentException if the method does not take exactly a {@link Request} * and a {@link Response}, or does not return {@code void} */ private static void validateSignature(Method m) { Class[] params = m.getParameterTypes(); if (params.length != 2 || params[0] != Request.class || params[1] != Response.class) { throw new IllegalArgumentException("Handler-Methode " + m + " muss Signatur (Request, Response) haben"); } if (m.getReturnType() != void.class) { throw new IllegalArgumentException("Handler-Methode " + m + " muss void zurückgeben"); } } /** * Normalizes a route-level path by ensuring it starts with a single leading slash. * * @param p the raw path from a route annotation, may be {@code null} or empty * @return the normalized path, or an empty string if {@code p} is {@code null} or empty */ private static String normalizePath(String p) { if (p == null || p.isEmpty()) return ""; return p.startsWith("/") ? p : "/" + p; } /** * Immutable carrier for the HTTP method and path extracted from a route annotation. * * @param method the HTTP method name (e.g. {@code "GET"}) * @param path the route path relative to the controller prefix */ private record RouteInfo(String method, String path) { } }