diff --git a/README.md b/README.md index 69e9fb0..1af850f 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,21 @@ IntegrationFlow katanaFlow = Katana.flow() .build(); ``` +### Routing with Parameters + +Routes can include path parameters using curly braces. Declare matching +parameters in your controller method: + +```java +@Get(path = "/users/{id}") +public String routeUser(String id, HttpRequest req) { + return "user:" + id; +} +``` + +The router will automatically extract the `id` value from requests like +`/users/123` and pass it to the controller method. + --- ## Documentation diff --git a/src/main/java/com/norwood/core/AnnotationProcessor.java b/src/main/java/com/norwood/core/AnnotationProcessor.java index c9ecbf2..27ac41a 100644 --- a/src/main/java/com/norwood/core/AnnotationProcessor.java +++ b/src/main/java/com/norwood/core/AnnotationProcessor.java @@ -6,7 +6,6 @@ import java.lang.reflect.Method; import java.net.http.HttpRequest; import java.util.List; -import java.util.function.BiFunction; import com.norwood.core.annotations.Inject; import com.norwood.routing.Route; @@ -64,7 +63,7 @@ private void routePost(Post a, Router router, Method method) { throw new RuntimeException("Route already defined with path: " + path); } - router.defineRoute(Route.post(path, createHandler(method))); + router.defineRoute(Route.post(path, method)); } private void routeGet(Get a, Router router, Method method) { @@ -73,26 +72,14 @@ private void routeGet(Get a, Router router, Method method) { throw new RuntimeException("Route already defined with path: " + path); } - router.defineRoute(Route.get(path, createHandler(method))); + router.defineRoute(Route.get(path, method)); } private Container container() { return KatanaCore.container; } - private BiFunction createHandler(Method method) { - return (instance, request) -> invokeMethod(method, instance, request); - } - - private Object invokeMethod(Method method, Object instance, Object arg1) { - try { - return method.invoke(instance, arg1); - } catch (IllegalAccessException | InvocationTargetException e) { - System.out.println("Error invoking stuff..."); - e.printStackTrace(); - throw new RuntimeException("Failed executing method: " + method.getName()); - } - } + // parameter-aware handler invocation is performed directly by Route } diff --git a/src/main/java/com/norwood/routing/Route.java b/src/main/java/com/norwood/routing/Route.java index 88a9863..fe553bf 100644 --- a/src/main/java/com/norwood/routing/Route.java +++ b/src/main/java/com/norwood/routing/Route.java @@ -1,8 +1,15 @@ package com.norwood.routing; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.http.HttpRequest; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Locale; -import java.util.function.BiFunction; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class Route { public enum HttpMethod { @@ -25,44 +32,89 @@ public String toString() { } } + private static final Pattern PLACEHOLDER = Pattern.compile("\\{([^/]+)\\}"); + private final HttpMethod method; - private final String path; - private final Object handler; + private final String pattern; + private final Pattern regex; + private final List parameterNames; + private final Method handlerMethod; - private Route(HttpMethod method, String name, Object handler) { + private Route(HttpMethod method, String pattern, Method handlerMethod) { this.method = method; - this.path = name; - this.handler = handler; + this.pattern = pattern; + this.handlerMethod = handlerMethod; + + Matcher matcher = PLACEHOLDER.matcher(pattern); + StringBuffer sb = new StringBuffer(); + List names = new ArrayList<>(); + while (matcher.find()) { + names.add(matcher.group(1)); + matcher.appendReplacement(sb, "([^/]+)"); + } + matcher.appendTail(sb); + this.regex = Pattern.compile("^" + sb.toString() + "$"); + this.parameterNames = List.copyOf(names); } - private static Route create(HttpMethod method, String path, Object handler) { - return new Route(method, path, handler); + private static Route create(HttpMethod method, String pattern, Method handlerMethod) { + return new Route(method, pattern, handlerMethod); } public boolean ofPath(String path) { - return this.path.equals(path); + return this.pattern.equals(path); } - public static Route get(String name, BiFunction handler) { - return Route.create(HttpMethod.GET, name, handler); + public boolean matches(String path) { + return regex.matcher(path).matches(); } - public static Route post(String name, BiFunction handler) { - return Route.create(HttpMethod.POST, name, handler); + public Map extract(String path) { + Matcher m = regex.matcher(path); + if (!m.matches()) { + return Map.of(); + } + Map map = new HashMap<>(); + for (int i = 0; i < parameterNames.size(); i++) { + map.put(parameterNames.get(i), m.group(i + 1)); + } + return map; + } + + public static Route get(String pattern, Method method) { + return Route.create(HttpMethod.GET, pattern, method); + } + + public static Route post(String pattern, Method method) { + return Route.create(HttpMethod.POST, pattern, method); } @Override public String toString() { - return "Route name '" + method.toString() + " " + path + "'"; + return "Route '" + method.toString() + " " + pattern + "'"; } - @SuppressWarnings("unchecked") - public BiFunction handler() { - return (BiFunction) handler; + public Object invoke(Object controller, HttpRequest request, Map params) { + try { + Class[] types = handlerMethod.getParameterTypes(); + Object[] args = new Object[types.length]; + int paramIdx = 0; + for (int i = 0; i < types.length; i++) { + if (HttpRequest.class.isAssignableFrom(types[i])) { + args[i] = request; + } else { + String name = parameterNames.get(paramIdx++); + args[i] = params.get(name); + } + } + return handlerMethod.invoke(controller, args); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } } public String path() { - return path; + return pattern; } } diff --git a/src/main/java/com/norwood/routing/Router.java b/src/main/java/com/norwood/routing/Router.java index eecf997..d165b7b 100644 --- a/src/main/java/com/norwood/routing/Router.java +++ b/src/main/java/com/norwood/routing/Router.java @@ -3,6 +3,7 @@ import java.net.http.HttpRequest; import java.util.ArrayList; import java.util.List; +import java.util.Map; import com.norwood.core.KatanaCore; import com.norwood.userland.UserController; @@ -11,18 +12,19 @@ public class Router { final List routes = new ArrayList<>(); public Object route(HttpRequest request) { - System.out.println(resolveController()); - return findRouteByPath(request).handler().apply(resolveController(), request); + Route route = findMatchingRoute(request); + Map params = route.extract(request.uri().getRawPath()); + return route.invoke(resolveController(), request, params); } private UserController resolveController() { return KatanaCore.container.get(UserController.class); } - private Route findRouteByPath(HttpRequest request) { + private Route findMatchingRoute(HttpRequest request) { String path = request.uri().getRawPath(); return routes.stream() - .filter(r -> r.ofPath(path)) + .filter(r -> r.matches(path)) .findFirst().orElseThrow(); } diff --git a/src/main/java/com/norwood/userland/UserController.java b/src/main/java/com/norwood/userland/UserController.java index f7f3952..776369c 100644 --- a/src/main/java/com/norwood/userland/UserController.java +++ b/src/main/java/com/norwood/userland/UserController.java @@ -40,4 +40,9 @@ public String route2(HttpRequest request) { throw new RuntimeException("Unable to return index.html"); } } + + @Get(path = "/users/{id}") + public String routeUser(String id, HttpRequest request) { + return "user:" + id; + } } diff --git a/src/test/java/com/norwood/RouterTest.java b/src/test/java/com/norwood/RouterTest.java new file mode 100644 index 0000000..10c7c59 --- /dev/null +++ b/src/test/java/com/norwood/RouterTest.java @@ -0,0 +1,32 @@ +package com.norwood; + +import java.lang.reflect.Method; +import java.net.URI; +import java.net.http.HttpRequest; + +import junit.framework.TestCase; + +import com.norwood.core.KatanaCore; +import com.norwood.routing.Route; +import com.norwood.routing.Router; +import com.norwood.userland.UserController; + +public class RouterTest extends TestCase { + public void testPathParameterRouting() throws Exception { + Router router = new Router(); + // register controller in container + try { + KatanaCore.container.set(UserController.class, new UserController()); + } catch (Exception ignored) {} + Method m = UserController.class.getMethod("routeUser", String.class, HttpRequest.class); + router.defineRoute(Route.get("/users/{id}", m)); + + HttpRequest req = HttpRequest.newBuilder() + .uri(new URI("http://localhost/users/123")) + .GET() + .build(); + + Object result = router.route(req); + assertEquals("user:123", result); + } +}