Files
Nextus-Web/src/main/java/dev/coph/nextusweb/server/annotation/AnnotationScanner.java
T

190 lines
7.7 KiB
Java

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}.
*
* <p>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:</p>
* <ol>
* <li>validates that the method has the required {@code (Request, Response)} signature and
* a {@code void} return type;</li>
* <li>creates a {@link MethodHandle} bound to the controller instance for fast,
* reflection-free invocation;</li>
* <li>registers a {@link Router.Handler} that delegates to that handle under the resolved
* HTTP method and full path.</li>
* </ol>
*
* <p>This class is a stateless utility and cannot be instantiated.</p>
*
* @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.
*
* <p>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.</p>
*
* @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) {
}
}