Skip to content

Commit dd75b76

Browse files
authored
Merge pull request #1566 from dirkroets/UNDERTOW-2303-faster-path-template-routing
[UNDERTOW-2303] Introduced a new, faster utility for routing path templates
2 parents 47e1b06 + 65c492b commit dd75b76

File tree

11 files changed

+4332
-178
lines changed

11 files changed

+4332
-178
lines changed

core/src/main/java/io/undertow/UndertowMessages.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,4 +661,18 @@ public interface UndertowMessages {
661661
@Message(id = 212, value = "Failed to encode query string '%s' with '%s' encoding.")
662662
IllegalArgumentException failedToEncodeQueryString(String q, String e);
663663

664+
@Message(id = 213, value = "Wild cards are only supported at the end of a template path")
665+
IllegalArgumentException wildCardsOnlyAtEndOfTemplate();
666+
667+
@Message(id = 214, value = "Illegal character '%s' is contained in the URL path segment '%s' at position %s")
668+
IllegalArgumentException illegalCharacterInPathSegment(char character, String segment, int position);
669+
670+
@Message(id = 215, value = "updateDefaultTargetFactory cannot be called after having added templates")
671+
IllegalStateException defaultTargetUpdatedAfterTemplatesAdded();
672+
673+
@Message(id = 216, value = "The builder already contains a template with the same pattern for '%s'")
674+
IllegalArgumentException duplicateUrlPathTemplate(String template);
675+
676+
@Message(id = 217, value = "The 'defaultTargetFactory' must be set before calling 'build'")
677+
IllegalStateException defaultTargetFactoryMustBeSetBeforeBuild();
664678
}

core/src/main/java/io/undertow/server/RoutingHandler.java

Lines changed: 395 additions & 131 deletions
Large diffs are not rendered by default.
Lines changed: 122 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/*
22
* JBoss, Home of Professional Open Source.
3-
* Copyright 2014 Red Hat, Inc., and individual contributors
3+
* Copyright 2025 Red Hat, Inc., and individual contributors
44
* as indicated by the @author tags.
55
*
66
* Licensed under the Apache License, Version 2.0 (the "License");
@@ -20,85 +20,154 @@
2020
import io.undertow.server.HttpHandler;
2121
import io.undertow.server.HttpServerExchange;
2222
import io.undertow.util.AttachmentKey;
23-
import io.undertow.util.PathTemplate;
24-
import io.undertow.util.PathTemplateMatcher;
25-
23+
import io.undertow.util.PathTemplateParser;
24+
import io.undertow.util.PathTemplateRouter;
25+
import io.undertow.util.PathTemplateRouteResult;
26+
import io.undertow.util.PathTemplateRouterFactory;
27+
import java.util.function.Supplier;
28+
import java.util.ArrayList;
29+
import java.util.List;
2630
import java.util.Map;
27-
import java.util.Set;
31+
import java.util.Objects;
2832
import java.util.stream.Collectors;
2933

3034
/**
31-
* A handler that matches URI templates
35+
* A drop-in substitute for the old PathTemplateHandler class. Ideally, one should use {@link PathTemplateRouterHandler} by
36+
* instantiating it with a {@link PathTemplateRouterFactory}. This class implements all of the methods from the original
37+
* PathTemplateHandler to provide backwards compatibility.
3238
*
33-
* @author Stuart Douglas
34-
* @see PathTemplateMatcher
39+
* @author Dirk Roets. This class was originally written by Stuart Douglas. After the introduction of
40+
* {@link PathTemplateRouterFactory}, it was rewritten against the original interface and tests.
3541
*/
3642
public class PathTemplateHandler implements HttpHandler {
3743

38-
private final boolean rewriteQueryParameters;
39-
40-
private final HttpHandler next;
41-
4244
/**
4345
* @see io.undertow.util.PathTemplateMatch#ATTACHMENT_KEY
4446
*/
4547
@Deprecated
46-
public static final AttachmentKey<PathTemplateMatch> PATH_TEMPLATE_MATCH = AttachmentKey.create(PathTemplateMatch.class);
48+
public static final AttachmentKey<PathTemplateHandler.PathTemplateMatch> PATH_TEMPLATE_MATCH = AttachmentKey.
49+
create(PathTemplateHandler.PathTemplateMatch.class);
4750

48-
private final PathTemplateMatcher<HttpHandler> pathTemplateMatcher = new PathTemplateMatcher<>();
51+
private final boolean rewriteQueryParameters;
52+
private final PathTemplateRouterFactory.SimpleBuilder<HttpHandler> builder;
53+
private final Object lock = new Object();
54+
private volatile PathTemplateRouter<HttpHandler> router;
4955

56+
/**
57+
* Default constructor. Uses {@link ResponseCodeHandler#HANDLE_404} as the next (default) handler and sets
58+
* 'rewriteQueryParameters' to 'true'.
59+
*/
5060
public PathTemplateHandler() {
5161
this(true);
5262
}
5363

54-
public PathTemplateHandler(boolean rewriteQueryParameters) {
64+
/**
65+
* Uses {@link ResponseCodeHandler#HANDLE_404} as the next (default) handler.
66+
*
67+
* @param rewriteQueryParameters Path parameters that are returned by the underlying router will be added as query
68+
* parameters to the exchange if this flag is 'true'.
69+
*/
70+
public PathTemplateHandler(final boolean rewriteQueryParameters) {
5571
this(ResponseCodeHandler.HANDLE_404, rewriteQueryParameters);
5672
}
5773

58-
public PathTemplateHandler(HttpHandler next) {
74+
/**
75+
* Sets 'rewriteQueryParameters' to 'true'.
76+
*
77+
* @param next The next (default) handler to use when requests do not match any of the specified templates.
78+
*/
79+
public PathTemplateHandler(final HttpHandler next) {
5980
this(next, true);
6081
}
6182

62-
public PathTemplateHandler(HttpHandler next, boolean rewriteQueryParameters) {
83+
/**
84+
* @param next The next (default) handler to use when requests do not match any of the specified templates.
85+
* @param rewriteQueryParameters Path parameters that are returned by the underlying router will be added as query
86+
* parameters to the exchange if this flag is 'true'.
87+
*/
88+
public PathTemplateHandler(final HttpHandler next, final boolean rewriteQueryParameters) {
89+
Objects.requireNonNull(next);
90+
6391
this.rewriteQueryParameters = rewriteQueryParameters;
64-
this.next = next;
92+
builder = PathTemplateRouterFactory.SimpleBuilder.newBuilder(next);
93+
router = builder.build();
6594
}
6695

67-
@Override
68-
public void handleRequest(HttpServerExchange exchange) throws Exception {
69-
PathTemplateMatcher.PathMatchResult<HttpHandler> match = pathTemplateMatcher.match(exchange.getRelativePath());
70-
if (match == null) {
71-
next.handleRequest(exchange);
72-
return;
73-
}
74-
exchange.putAttachment(PATH_TEMPLATE_MATCH, new PathTemplateMatch(match.getMatchedTemplate(), match.getParameters()));
75-
exchange.putAttachment(io.undertow.util.PathTemplateMatch.ATTACHMENT_KEY, new io.undertow.util.PathTemplateMatch(match.getMatchedTemplate(), match.getParameters()));
76-
if (rewriteQueryParameters) {
77-
for (Map.Entry<String, String> entry : match.getParameters().entrySet()) {
78-
exchange.addQueryParam(entry.getKey(), entry.getValue());
79-
}
96+
/**
97+
* Adds a template and handler to the underlying router.
98+
*
99+
* @param uriTemplate The URI path template.
100+
* @param handler The handler to use for requests that match the specified template.
101+
*
102+
* @return Reference to this handler.
103+
*/
104+
public PathTemplateHandler add(final String uriTemplate, final HttpHandler handler) {
105+
Objects.requireNonNull(uriTemplate);
106+
Objects.requireNonNull(handler);
107+
108+
// PathTemplateRouter builders are not thread-safe, so we need to synchronize.
109+
synchronized (lock) {
110+
builder.addTemplate(uriTemplate, handler);
111+
router = builder.build();
80112
}
81-
match.getValue().handleRequest(exchange);
82-
}
83113

84-
public PathTemplateHandler add(final String uriTemplate, final HttpHandler handler) {
85-
pathTemplateMatcher.add(uriTemplate, handler);
86114
return this;
87115
}
88116

117+
/**
118+
* Removes a template from the underlying router.
119+
*
120+
* @param uriTemplate The URI path template.
121+
*
122+
* @return Reference to this handler.
123+
*/
89124
public PathTemplateHandler remove(final String uriTemplate) {
90-
pathTemplateMatcher.remove(uriTemplate);
125+
Objects.requireNonNull(uriTemplate);
126+
127+
// PathTemplateRouter builders are not thread-safe, so we need to synchronize.
128+
synchronized (lock) {
129+
builder.removeTemplate(uriTemplate);
130+
router = builder.build();
131+
}
132+
91133
return this;
92134
}
93135

94136
@Override
95137
public String toString() {
96-
Set<PathTemplate> paths = pathTemplateMatcher.getPathTemplates();
97-
if (paths.size() == 1) {
98-
return "path-template( " + paths.toArray()[0] + " )";
138+
final List<PathTemplateParser.PathTemplatePatternEqualsAdapter<PathTemplateParser.PathTemplate<Supplier<HttpHandler>>>> templates
139+
= new ArrayList<>(builder.getBuilder().getTemplates().keySet());
140+
141+
final StringBuilder sb = new StringBuilder();
142+
sb.append("path-template( ");
143+
if (templates.size() == 1) {
144+
sb.append(templates.get(0).getPattern().getPathTemplate()).append(" )");
99145
} else {
100-
return "path-template( {" + paths.stream().map(s -> s.getTemplateString().toString()).collect(Collectors.joining(", ")) + "} )";
146+
sb.append('{').append(
147+
// Creates a ", " separated string of all patterns in this handler.
148+
templates.stream().map(s -> s.getPattern().getPathTemplate()).collect(Collectors.joining(", "))
149+
).append("} )");
150+
}
151+
return sb.toString();
152+
}
153+
154+
@Override
155+
public void handleRequest(final HttpServerExchange exchange) throws Exception {
156+
final PathTemplateRouteResult<HttpHandler> routeResult = router.route(exchange.getRelativePath());
157+
if (routeResult.getPathTemplate().isEmpty()) {
158+
// This is the default handler, therefore it doesn't contain path parameters.
159+
routeResult.getTarget().handleRequest(exchange);
160+
return;
101161
}
162+
163+
exchange.putAttachment(PATH_TEMPLATE_MATCH, new PathTemplateMatch(routeResult));
164+
exchange.putAttachment(io.undertow.util.PathTemplateMatch.ATTACHMENT_KEY, routeResult);
165+
if (rewriteQueryParameters) {
166+
for (Map.Entry<String, String> entry : routeResult.getParameters().entrySet()) {
167+
exchange.addQueryParam(entry.getKey(), entry.getValue());
168+
}
169+
}
170+
routeResult.getTarget().handleRequest(exchange);
102171
}
103172

104173
/**
@@ -107,20 +176,26 @@ public String toString() {
107176
@Deprecated
108177
public static final class PathTemplateMatch {
109178

110-
private final String matchedTemplate;
111-
private final Map<String, String> parameters;
179+
private final io.undertow.util.PathTemplateMatch pathTemplateMatch;
180+
181+
PathTemplateMatch(
182+
final io.undertow.util.PathTemplateMatch pathTemplateMatch
183+
) {
184+
this.pathTemplateMatch = Objects.requireNonNull(pathTemplateMatch);
185+
}
112186

113-
public PathTemplateMatch(String matchedTemplate, Map<String, String> parameters) {
114-
this.matchedTemplate = matchedTemplate;
115-
this.parameters = parameters;
187+
public PathTemplateMatch(final String matchedTemplate, final Map<String, String> parameters) {
188+
this(
189+
new io.undertow.util.PathTemplateMatch(matchedTemplate, parameters)
190+
);
116191
}
117192

118193
public String getMatchedTemplate() {
119-
return matchedTemplate;
194+
return pathTemplateMatch.getMatchedTemplate();
120195
}
121196

122197
public Map<String, String> getParameters() {
123-
return parameters;
198+
return pathTemplateMatch.getParameters();
124199
}
125200
}
126201
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* JBoss, Home of Professional Open Source.
3+
* Copyright 2025 Red Hat, Inc., and individual contributors
4+
* as indicated by the @author tags.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package io.undertow.server.handlers;
19+
20+
import io.undertow.server.HttpHandler;
21+
import io.undertow.server.HttpServerExchange;
22+
import io.undertow.util.PathTemplateRouter;
23+
import io.undertow.util.PathTemplateRouteResult;
24+
import io.undertow.util.PathTemplateRouterFactory;
25+
import java.util.Map;
26+
import java.util.Objects;
27+
28+
import static io.undertow.server.handlers.PathTemplateHandler.PATH_TEMPLATE_MATCH;
29+
30+
/**
31+
* A handler that matches URI templates.
32+
*
33+
* @author Dirk Roets
34+
* @see PathTemplateRouterFactory
35+
*/
36+
public class PathTemplateRouterHandler implements HttpHandler {
37+
38+
private final PathTemplateRouter<HttpHandler> router;
39+
private final boolean rewriteQueryParameters;
40+
41+
/**
42+
* @param router The path template router to use.
43+
* @param rewriteQueryParameters Path parameters that are returned by the specified router will be added as query parameters
44+
* to the exchange if this flag is 'true'.
45+
*/
46+
public PathTemplateRouterHandler(
47+
final PathTemplateRouter<HttpHandler> router,
48+
final boolean rewriteQueryParameters
49+
) {
50+
this.router = Objects.requireNonNull(router);
51+
this.rewriteQueryParameters = rewriteQueryParameters;
52+
}
53+
54+
@Override
55+
public void handleRequest(final HttpServerExchange exchange) throws Exception {
56+
final PathTemplateRouteResult<HttpHandler> routeResult = router.route(exchange.getRelativePath());
57+
if (routeResult.getPathTemplate().isEmpty()) {
58+
// This is the default handler, therefore it doesn't contain path parameters.
59+
routeResult.getTarget().handleRequest(exchange);
60+
return;
61+
}
62+
63+
exchange.putAttachment(PATH_TEMPLATE_MATCH, new PathTemplateHandler.PathTemplateMatch(routeResult));
64+
exchange.putAttachment(io.undertow.util.PathTemplateMatch.ATTACHMENT_KEY, routeResult);
65+
if (rewriteQueryParameters && !routeResult.getParameters().isEmpty()) {
66+
for (final Map.Entry<String, String> entry : routeResult.getParameters().entrySet()) {
67+
exchange.addQueryParam(entry.getKey(), entry.getValue());
68+
}
69+
}
70+
routeResult.getTarget().handleRequest(exchange);
71+
}
72+
}

core/src/main/java/io/undertow/util/PathTemplateMatcher.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,10 @@
3636
* TODO: we can probably do this faster using a trie type structure, but I think the current impl should perform ok most of the time
3737
*
3838
* @author Stuart Douglas
39+
*
40+
* @deprecated See {@link PathTemplateRouter}.
3941
*/
42+
@Deprecated
4043
public class PathTemplateMatcher<T> {
4144

4245
/**

0 commit comments

Comments
 (0)