dev.mccue.jdk.httpserver.regexrouter.RegexRouter Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jdk-httpserver-regexrouter Show documentation
Show all versions of jdk-httpserver-regexrouter Show documentation
Request router and dispatcher for the JDK's built-in HTTP server
The newest version!
package dev.mccue.jdk.httpserver.regexrouter;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import org.intellij.lang.annotations.Language;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Function;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Implementation of a Router that just does a linear scan through regexes.
*
*
* This was chosen for my demo since it is the easiest thing to implement, not because it
* is the most performant.
*
*
*
* That being said, this seems to be roughly how Django handles routing.
*
*
*
* From the django docs
*
*
*
* - Django runs through each URL pattern, in order, and stops at the first one that matches the requested URL, matching against path_info.
* - Once one of the URL patterns matches, Django imports and calls the given view, which is a Python function (or a class-based view). The view gets passed the following arguments:
* -
*
* - An instance of HttpRequest.
* - If the matched URL pattern contained no named groups, then the matches from the regular expression are provided as positional arguments.
* - The keyword arguments are made up of any named parts matched by the path expression that are provided, overridden by any arguments specified in the optional kwargs argument to django.urls.path() or django.urls.re_path().
*
*
*
*
*
* For the syntax of declaring a named group, this stack overflow question should be useful.
*
*/
public final class RegexRouter implements HttpHandler {
private final List mappings;
private final ErrorHandler errorHandler;
private final HttpHandler notFoundHandler;
private RegexRouter(Builder builder) {
this.mappings = builder.mappings;
this.errorHandler = builder.errorHandler;
this.notFoundHandler = builder.notFoundHandler;
}
/**
* Creates a {@link Builder}.
*
* @return A builder.
*/
public static Builder builder() {
return new Builder();
}
/**
* Takes a matcher and provides an implementation of RouteParams on top of it.
*
*
* Makes the assumption that it takes ownership of the matcher and will be exposed only via
* the interface, so the mutability of the matcher is not relevant.
*
*/
record MatcherRouteParams(MatchResult matchResult) implements RouteParams {
@Override
public Optional param(int pos) {
if (matchResult.groupCount() > pos - 1 || pos < 0) {
return Optional.empty();
}
else {
return Optional.of(URLDecoder.decode(matchResult.group(pos + 1), StandardCharsets.UTF_8));
}
}
@Override
public Optional param(String name) {
try {
final var namedGroup = matchResult.group(name);
if (namedGroup == null) {
return Optional.empty();
}
else {
return Optional.of(URLDecoder.decode(namedGroup, StandardCharsets.UTF_8));
}
} catch (IllegalArgumentException ex) {
// Yes this is bad, but there is no interface that a matcher gives
// for verifying whether a named group even exists in the pattern.
// If no match exists it will throw this exception, so for better or worse
// this should be okay. JDK never changes.
return Optional.empty();
}
}
}
/**
* Handles the request if there is a matching handler.
* @param exchange The request to handle.
*/
@Override
public void handle(HttpExchange exchange) throws IOException {
for (final var mapping : this.mappings) {
final var method = exchange.getRequestMethod();
final var pattern = mapping.routePattern();
final var matcher = pattern.matcher(exchange.getRequestURI().getPath());
if (method.equalsIgnoreCase(mapping.method) && matcher.matches()) {
new MatcherRouteParams(matcher.toMatchResult()).set(exchange);
try {
mapping.handler.handle(exchange);
} catch (Throwable t) {
errorHandler.handle(t, exchange);
}
return;
}
}
notFoundHandler.handle(exchange);
}
private record Mapping(
String method,
Pattern routePattern,
HttpHandler handler
) {}
/**
* A builder for {@link RegexRouter}.
*/
public static final class Builder {
private final List mappings;
private ErrorHandler errorHandler;
private HttpHandler notFoundHandler;
private Builder() {
this.mappings = new ArrayList<>();
this.errorHandler = new InternalErrorHandler();
this.notFoundHandler = new NotFoundHandler();
}
public Builder route(
String method,
Pattern routePattern,
HttpHandler handler
) {
return route(List.of(method), routePattern, handler);
}
public Builder route(
List methods,
Pattern routePattern,
HttpHandler handler
) {
Objects.requireNonNull(methods);
Objects.requireNonNull(routePattern);
Objects.requireNonNull(handler);
if (!methods.isEmpty()) {
for (var method : methods) {
this.mappings.add(new Mapping(
method.toLowerCase(),
routePattern,
handler
));
}
}
return this;
}
public Builder get(Pattern routePattern, HttpHandler handler) {
return route("get", routePattern, handler);
}
public Builder post(Pattern routePattern, HttpHandler handler) {
return route("post", routePattern, handler);
}
public Builder patch(Pattern routePattern, HttpHandler handler) {
return route("patch", routePattern, handler);
}
public Builder put(Pattern routePattern, HttpHandler handler) {
return route("put", routePattern, handler);
}
public Builder head(Pattern routePattern, HttpHandler handler) {
return route("head", routePattern, handler);
}
public Builder delete(Pattern routePattern, HttpHandler handler) {
return route("delete", routePattern, handler);
}
public Builder options(Pattern routePattern, HttpHandler handler) {
return route("options", routePattern, handler);
}
public Builder route(
String method,
@Language("RegExp") String routePattern,
HttpHandler handler
) {
return route(method, Pattern.compile(routePattern), handler);
}
public Builder route(
List methods,
@Language("RegExp") String routePattern,
HttpHandler handler
) {
return route(methods, Pattern.compile(routePattern), handler);
}
public Builder get(@Language("RegExp") String routePattern, HttpHandler handler) {
return get(Pattern.compile(routePattern), handler);
}
public Builder post(@Language("RegExp") String routePattern, HttpHandler handler) {
return post(Pattern.compile(routePattern), handler);
}
public Builder patch(@Language("RegExp") String routePattern, HttpHandler handler) {
return patch(Pattern.compile(routePattern), handler);
}
public Builder put(@Language("RegExp") String routePattern, HttpHandler handler) {
return put(Pattern.compile(routePattern), handler);
}
public Builder head(@Language("RegExp") String routePattern, HttpHandler handler) {
return head(Pattern.compile(routePattern), handler);
}
public Builder delete(@Language("RegExp") String routePattern, HttpHandler handler) {
return delete(Pattern.compile(routePattern), handler);
}
public Builder options(@Language("RegExp") String routePattern, HttpHandler handler) {
return options(Pattern.compile(routePattern), handler);
}
public Builder errorHandler(Function errorHandler) {
this.errorHandler = (t, exchange) -> errorHandler.apply(t).handle(exchange);
return this;
}
public Builder errorHandler(ErrorHandler errorHandler) {
this.errorHandler = errorHandler;
return this;
}
public Builder notFoundHandler(HttpHandler handler) {
this.notFoundHandler = handler;
return this;
}
/**
* Builds the {@link RegexRouter}.
* @return The built router, ready to handle requests.
*/
public RegexRouter build() {
return new RegexRouter(this);
}
}
public interface ErrorHandler {
void handle(Throwable error, HttpExchange exchange) throws IOException;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy