I added my own router handler that does everything I need it to do. It's pretty well documented and very well tested. I'd be willing to submit a pull request to have this added to Undertow if that'd be valuable to anyone. Here's a taste of the code (the core stuff is in PathTrie):
package com.analyticspot.uservices.server.router;
import com.analyticspot.httputils.HttpVerb;
import com.analyticspot.httputils.ResponseCodes;
import com.memoizrlabs.retrooptional.Optional;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.AttachmentKey;
import java.util.Map;
import java.util.TreeMap;
/**
* An HttpHandler that routes requests to other handlers based on paths and path templates. It is thread safe to
* search this class concurrently (match paths) but adding paths is not thread safe. Thus the expected use-case is
* that all paths are set up before the handler is added as an Undertow listener.
*
* <p>The rules for path matching are:
* <ul>
* <li>All paths must begin with a "/"</li>
* <li>Wildcard matches are allowed by adding "{X}" to the path. Then {@code X} and what matched it are attached
* to the request in a {@code Map} from {@code X} to the value that matched. For example, if the added path was
* "/foo/{name}/stuff/{age}" and the observed path was "/foo/bar/stuff/22" then the attachment would have a map
* like "{name: bar, age: 22}.</li>
* <li>Prefix paths are allowed. Such paths will match anything that begins with the provided prefix.</li>
* <li>Exact matches and/or longer matches take precedence over both prefix and wildcard matches. Thus, given the
* wildcard path "/foo/{name}", the prefix path "/foo/baz", and the exact path "/foo/bar", we would expect the
* following matches:
* <table>
* <tr><td>Observed Path</td><td>Matched Path</td></tr>
* <tr><td>/foo/bar</td><td>/foo/bar</td></tr>
* <tr><td>/foo/baz</td><td>/foo/baz</td></tr>
* <tr><td>/foo/baz/bump</td><td>/foo/baz</td></tr>
* <tr><td>/foo/gourdy</td><td>/foo/{name}</td></tr>
* </table>
* </li>
* <li>This is not a "backtracking" matcher. Thus, given a prefix path of "/foo/bar" and an exact path
* "/foo/bar/baz", the observed path "/foo/bar/baz/bizzle" would have <b>no match</b> because "/foo/bar/baz"
* doesn't match and we do not backtrack to the shorter "/foo/bar" prefix to see if it matches.</li>
* <li>If both a prefix and a wildcard match, the wildcard takes precendence. Thus given prefix path "/foo" and
* wildcard path "/foo/{name}", the match for obeserved path "/foo/bar" would be the wildcard path.</li>
* </ul>
*
* <p>We added our own router due to limitations in the Undertow PathTemplateHandler and RoutingHandler. See
*/
public class PathRoutingHandler implements HttpHandler {
public static AttachmentKey<PathMatch> PATH_MATCH_KEY = AttachmentKey.create(PathMatch.class);
// Map from HTTP verb to the PathTrie for the handlers for that route.
private final Map<HttpVerb, PathTrie> verbToPathTrie = new TreeMap<>();
// If present and none of the paths match this handler will be called.
Optional<HttpHandler> fallbackHandler;
public PathRoutingHandler() {
fallbackHandler = Optional.empty();
}
/**
* Provides a fallback handler which will handle the request if nothing in the trie matches.
*/
public PathRoutingHandler(HttpHandler fallbackHandler) {
this.fallbackHandler = Optional.of(fallbackHandler);
}
/**
* Adds a handler for the given URL/verb combination. The path can be a template as per the class comment.
*/
public void addHandler(HttpVerb verb, String path, HttpHandler handler) {
PathTrie trie = getTrie(verb);
trie.addPath(path, handler);
}
/**
* Like {@link #addHandler(HttpVerb, String, HttpHandler)}but add a prefix handler.
*/
public void addPrefixHandler(HttpVerb verb, String path, HttpHandler handler) {
PathTrie trie = getTrie(verb);
trie.addPrefixPath(path, handler);
}
private PathTrie getTrie(HttpVerb verb) {
if (!verbToPathTrie.containsKey(verb)) {
PathTrie trie = new PathTrie();
verbToPathTrie.put(verb, trie);
return trie;
} else {
return verbToPathTrie.get(verb);
}
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
HttpVerb verb = HttpVerb.fromString(exchange.getRequestMethod().toString());
PathTrie trie = verbToPathTrie.get(verb);
if (trie == null) {
handleNoMatch(exchange);
} else {
Optional<PathMatch> optMatch = trie.findMatch(exchange.getRelativePath());
if (optMatch.isPresent()) {
exchange.putAttachment(PATH_MATCH_KEY, optMatch.get());
optMatch.get().getHandler().handleRequest(exchange);
} else {
handleNoMatch(exchange);
}
}
}
private void handleNoMatch(HttpServerExchange exchange) throws Exception {
if (fallbackHandler.isPresent()) {
fallbackHandler.get().handleRequest(exchange);
} else {
exchange.setStatusCode(ResponseCodes.NOT_FOUND.getCode());
exchange.endExchange();
}
}
}