Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 3 additions & 16 deletions src/main/java/com/norwood/core/AnnotationProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand All @@ -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<Object, HttpRequest, Object> 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

}

88 changes: 70 additions & 18 deletions src/main/java/com/norwood/routing/Route.java
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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<String> 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<String> 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<Object, HttpRequest, Object> 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<Object, HttpRequest, Object> handler) {
return Route.create(HttpMethod.POST, name, handler);
public Map<String, String> extract(String path) {
Matcher m = regex.matcher(path);
if (!m.matches()) {
return Map.of();
}
Map<String, String> 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<Object, HttpRequest, Object> handler() {
return (BiFunction<Object, HttpRequest, Object>) handler;
public Object invoke(Object controller, HttpRequest request, Map<String, String> 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;
}

}
10 changes: 6 additions & 4 deletions src/main/java/com/norwood/routing/Router.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -11,18 +12,19 @@ public class Router {
final List<Route> routes = new ArrayList<>();

public Object route(HttpRequest request) {
System.out.println(resolveController());
return findRouteByPath(request).handler().apply(resolveController(), request);
Route route = findMatchingRoute(request);
Map<String, String> 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();
}

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/norwood/userland/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
32 changes: 32 additions & 0 deletions src/test/java/com/norwood/RouterTest.java
Original file line number Diff line number Diff line change
@@ -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);
}
}