Skip to content

Commit 1306db6

Browse files
committed
Support handler ordering in UrlHandlerFilter
1 parent 269f251 commit 1306db6

File tree

4 files changed

+567
-83
lines changed

4 files changed

+567
-83
lines changed

spring-web/src/main/java/org/springframework/web/filter/UrlHandlerFilter.java

Lines changed: 230 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818

1919
import java.io.IOException;
2020
import java.util.Arrays;
21+
import java.util.Collections;
2122
import java.util.List;
2223
import java.util.Map;
24+
import java.util.TreeMap;
2325
import java.util.function.Consumer;
2426

2527
import jakarta.servlet.DispatcherType;
@@ -39,6 +41,7 @@
3941
import org.springframework.util.Assert;
4042
import org.springframework.util.LinkedMultiValueMap;
4143
import org.springframework.util.MultiValueMap;
44+
import org.springframework.util.ObjectUtils;
4245
import org.springframework.util.StringUtils;
4346
import org.springframework.web.util.ServletRequestPathUtils;
4447
import org.springframework.web.util.pattern.PathPattern;
@@ -54,25 +57,26 @@
5457
* UrlHandlerFilter filter = UrlHandlerFilter
5558
* .trailingSlashHandler("/path1/**").redirect(HttpStatus.PERMANENT_REDIRECT)
5659
* .trailingSlashHandler("/path2/**").wrapRequest()
60+
* .exclude("/path1/foo/bar/**", "/path2/baz/")
5761
* .build();
5862
* </pre>
5963
*
6064
* <p>This {@code Filter} should be ordered after {@link ForwardedHeaderFilter},
6165
* before {@link ServletRequestPathFilter}, and before security filters.
6266
*
63-
* @author Rossen Stoyanchev
67+
* @author Rossen Stoyanchev, James Missen
6468
* @since 6.2
6569
*/
6670
public final class UrlHandlerFilter extends OncePerRequestFilter {
6771

6872
private static final Log logger = LogFactory.getLog(UrlHandlerFilter.class);
6973

7074

71-
private final MultiValueMap<Handler, PathPattern> handlers;
75+
private final HandlerRegistry handlerRegistry;
7276

7377

74-
private UrlHandlerFilter(MultiValueMap<Handler, PathPattern> handlers) {
75-
this.handlers = new LinkedMultiValueMap<>(handlers);
78+
private UrlHandlerFilter(HandlerRegistry handlerRegistry) {
79+
this.handlerRegistry = handlerRegistry;
7680
}
7781

7882

@@ -83,33 +87,26 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
8387
RequestPath path = (ServletRequestPathUtils.hasParsedRequestPath(request) ?
8488
ServletRequestPathUtils.getParsedRequestPath(request) :
8589
ServletRequestPathUtils.parse(request));
86-
87-
for (Map.Entry<Handler, List<PathPattern>> entry : this.handlers.entrySet()) {
88-
if (!entry.getKey().supports(request, path)) {
89-
continue;
90-
}
91-
for (PathPattern pattern : entry.getValue()) {
92-
if (pattern.matches(path)) {
93-
entry.getKey().handle(request, response, chain);
94-
return;
95-
}
96-
}
90+
PathContainer lookupPath = path.subPath(path.contextPath().elements().size());
91+
Handler handler = handlerRegistry.lookupHandler(path, lookupPath, request);
92+
if (handler != null) {
93+
handler.handle(request, response, chain);
94+
return;
9795
}
98-
9996
chain.doFilter(request, response);
10097
}
10198

10299

103100
/**
104-
* Create a builder by adding a handler for URL's with a trailing slash.
105-
* @param pathPatterns path patterns to map the handler to, for example,
101+
* Create a builder by adding a handler for URLs with a trailing slash.
102+
* @param patterns path patterns to map the handler to, for example,
106103
* <code>"/path/&#42;"</code>, <code>"/path/&#42;&#42;"</code>,
107104
* <code>"/path/foo/"</code>.
108105
* @return a spec to configure the trailing slash handler with
109106
* @see Builder#trailingSlashHandler(String...)
110107
*/
111-
public static Builder.TrailingSlashSpec trailingSlashHandler(String... pathPatterns) {
112-
return new DefaultBuilder().trailingSlashHandler(pathPatterns);
108+
public static Builder.TrailingSlashSpec trailingSlashHandler(String... patterns) {
109+
return new DefaultBuilder().trailingSlashHandler(patterns);
113110
}
114111

115112

@@ -119,13 +116,32 @@ public static Builder.TrailingSlashSpec trailingSlashHandler(String... pathPatte
119116
public interface Builder {
120117

121118
/**
122-
* Add a handler for URL's with a trailing slash.
123-
* @param pathPatterns path patterns to map the handler to, for example,
119+
* Add a handler for URLs with a trailing slash.
120+
* @param patterns path patterns to map the handler to, for example,
124121
* <code>"/path/&#42;"</code>, <code>"/path/&#42;&#42;"</code>,
125122
* <code>"/path/foo/"</code>.
126123
* @return a spec to configure the handler with
127124
*/
128-
TrailingSlashSpec trailingSlashHandler(String... pathPatterns);
125+
TrailingSlashSpec trailingSlashHandler(String... patterns);
126+
127+
/**
128+
* Exclude patterns from matching any other handler.
129+
* @param patterns path patterns to not map any handler to, e.g.
130+
* <code>"/path/foo/&#42;"</code>, <code>"/path/foo/&#42;&#42;"</code>,
131+
* <code>"/path/foo/bar/"</code>.
132+
* @return the {@link Builder}, which allows adding more
133+
* handlers and then building the Filter instance.
134+
*/
135+
Builder exclude(String... patterns);
136+
137+
/**
138+
* Specify whether to use path pattern specificity for matching handlers,
139+
* with more specific patterns taking precedence.
140+
* <p>The default value is {@code false}.
141+
* @return the {@link Builder}, which allows adding more
142+
* handlers and then building the Filter instance.
143+
*/
144+
Builder useSpecificityOrder(boolean useSpecificityOrder);
129145

130146
/**
131147
* Build the {@link UrlHandlerFilter} instance.
@@ -169,36 +185,89 @@ interface TrailingSlashSpec {
169185
*/
170186
private static final class DefaultBuilder implements Builder {
171187

172-
private final PathPatternParser patternParser = new PathPatternParser();
188+
/**
189+
* Empty handler that does not handle the request URL, and proceeds directly to the next filter.
190+
*/
191+
private static final Handler NO_OP_HANDLER = new Handler() {
192+
193+
@Override
194+
public boolean supports(HttpServletRequest request, RequestPath path) {
195+
return true;
196+
}
197+
198+
@Override
199+
public void handle(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
200+
throws ServletException, IOException {
201+
202+
chain.doFilter(request, response);
203+
}
204+
205+
@Override
206+
public String toString() {
207+
return "NoOpHandler";
208+
}
209+
};
210+
211+
private final MultiValueMap<Handler, String> handlers = new LinkedMultiValueMap<>();
173212

174-
private final MultiValueMap<Handler, PathPattern> handlers = new LinkedMultiValueMap<>();
213+
private boolean useSpecificityOrder = false;
214+
215+
private DefaultBuilder() {
216+
// Ensure any no-op handlers are registered first when building
217+
this.handlers.addAll(NO_OP_HANDLER, Collections.emptyList());
218+
}
175219

176220
@Override
177221
public TrailingSlashSpec trailingSlashHandler(String... patterns) {
178222
return new DefaultTrailingSlashSpec(patterns);
179223
}
180224

181-
private DefaultBuilder addHandler(List<PathPattern> pathPatterns, Handler handler) {
182-
pathPatterns.forEach(pattern -> this.handlers.add(handler, pattern));
225+
@Override
226+
public Builder exclude(String... patterns) {
227+
return addHandler(NO_OP_HANDLER, patterns);
228+
}
229+
230+
@Override
231+
public Builder useSpecificityOrder(boolean useSpecificityOrder) {
232+
this.useSpecificityOrder = useSpecificityOrder;
233+
return this;
234+
}
235+
236+
private Builder addHandler(Handler handler, String... patterns) {
237+
if (!ObjectUtils.isEmpty(patterns)) {
238+
this.handlers.addAll(handler, Arrays.stream(patterns).toList());
239+
}
183240
return this;
184241
}
185242

186243
@Override
187244
public UrlHandlerFilter build() {
188-
return new UrlHandlerFilter(this.handlers);
245+
HandlerRegistry handlerRegistry;
246+
if (this.useSpecificityOrder) {
247+
handlerRegistry = new OrderedHandlerRegistry();
248+
}
249+
else {
250+
handlerRegistry = new DefaultHandlerRegistry();
251+
}
252+
for (Map.Entry<Handler, List<String>> entry : this.handlers.entrySet()) {
253+
for (String pattern : entry.getValue()) {
254+
handlerRegistry.registerHandler(pattern, entry.getKey());
255+
}
256+
}
257+
258+
return new UrlHandlerFilter(handlerRegistry);
189259
}
190260

191261
private final class DefaultTrailingSlashSpec implements TrailingSlashSpec {
192262

193-
private final List<PathPattern> pathPatterns;
263+
private final String[] pathPatterns;
194264

195265
private @Nullable Consumer<HttpServletRequest> interceptor;
196266

197267
private DefaultTrailingSlashSpec(String[] patterns) {
198268
this.pathPatterns = Arrays.stream(patterns)
199269
.map(pattern -> pattern.endsWith("**") || pattern.endsWith("/") ? pattern : pattern + "/")
200-
.map(patternParser::parse)
201-
.toList();
270+
.toArray(String[]::new);
202271
}
203272

204273
@Override
@@ -210,19 +279,18 @@ public TrailingSlashSpec intercept(Consumer<HttpServletRequest> consumer) {
210279
@Override
211280
public Builder redirect(HttpStatusCode statusCode) {
212281
Handler handler = new RedirectTrailingSlashHandler(statusCode, this.interceptor);
213-
return DefaultBuilder.this.addHandler(this.pathPatterns, handler);
282+
return DefaultBuilder.this.addHandler(handler, this.pathPatterns);
214283
}
215284

216285
@Override
217286
public Builder wrapRequest() {
218287
Handler handler = new RequestWrappingTrailingSlashHandler(this.interceptor);
219-
return DefaultBuilder.this.addHandler(this.pathPatterns, handler);
288+
return DefaultBuilder.this.addHandler(handler, this.pathPatterns);
220289
}
221290
}
222291
}
223292

224293

225-
226294
/**
227295
* Internal handler to encapsulate different ways to handle a request.
228296
*/
@@ -281,6 +349,11 @@ protected String trimTrailingSlash(String path) {
281349
int index = (StringUtils.hasLength(path) ? path.lastIndexOf('/') : -1);
282350
return (index != -1 ? path.substring(0, index) : path);
283351
}
352+
353+
@Override
354+
public String toString() {
355+
return getClass().getSimpleName();
356+
}
284357
}
285358

286359

@@ -312,6 +385,11 @@ public void handleInternal(HttpServletRequest request, HttpServletResponse respo
312385
response.setHeader(HttpHeaders.LOCATION, location);
313386
response.flushBuffer();
314387
}
388+
389+
@Override
390+
public String toString() {
391+
return getClass().getSimpleName() + " {statusCode=" + this.statusCode.value() + "}";
392+
}
315393
}
316394

317395

@@ -405,4 +483,122 @@ private HttpServletRequest getDelegate() {
405483
}
406484
}
407485

486+
487+
/**
488+
* Internal registry to encapsulate different ways to select a handler for a request.
489+
*/
490+
private interface HandlerRegistry {
491+
492+
/**
493+
* Register the specified handler for the given path pattern.
494+
* @param pattern the path pattern the handler should be mapped to
495+
* @param handler the handler instance to register
496+
* @throws IllegalStateException if there is a conflicting handler registered
497+
*/
498+
void registerHandler(String pattern, Handler handler);
499+
500+
/**
501+
* Look up a handler instance for the given URL lookup path.
502+
* @param path the parsed RequestPath
503+
* @param lookupPath the URL path the handler is mapped to
504+
* @param request the current request
505+
* @return the associated handler instance, or {@code null} if not found
506+
* @see org.springframework.web.util.pattern.PathPattern
507+
*/
508+
@Nullable Handler lookupHandler(RequestPath path, PathContainer lookupPath, HttpServletRequest request);
509+
}
510+
511+
512+
/**
513+
* Base class for {@link HandlerRegistry} implementations.
514+
*/
515+
private static abstract class AbstractHandlerRegistry implements HandlerRegistry {
516+
517+
private final PathPatternParser patternParser = new PathPatternParser();
518+
519+
@Override
520+
public final void registerHandler(String pattern, Handler handler) {
521+
Assert.notNull(pattern, "Pattern must not be null");
522+
Assert.notNull(handler, "Handler must not be null");
523+
524+
// Parse path pattern
525+
pattern = patternParser.initFullPathPattern(pattern);
526+
PathPattern pathPattern = patternParser.parse(pattern);
527+
528+
// Register handler
529+
registerHandlerInternal(pathPattern, handler);
530+
if (logger.isTraceEnabled()) {
531+
logger.trace("Mapped [" + pattern + "] onto " + handler);
532+
}
533+
}
534+
535+
protected abstract void registerHandlerInternal(PathPattern pathPattern, Handler handler);
536+
}
537+
538+
539+
/**
540+
* Default {@link HandlerRegistry} implementation.
541+
*/
542+
private static final class DefaultHandlerRegistry extends AbstractHandlerRegistry {
543+
544+
private final MultiValueMap<Handler, PathPattern> handlerMap = new LinkedMultiValueMap<>();
545+
546+
@Override
547+
protected void registerHandlerInternal(PathPattern pathPattern, Handler handler) {
548+
this.handlerMap.add(handler, pathPattern);
549+
}
550+
551+
@Override
552+
public @Nullable Handler lookupHandler(RequestPath path, PathContainer lookupPath, HttpServletRequest request) {
553+
for (Map.Entry<Handler, List<PathPattern>> entry : this.handlerMap.entrySet()) {
554+
if (!entry.getKey().supports(request, path)) {
555+
continue;
556+
}
557+
for (PathPattern pattern : entry.getValue()) {
558+
if (pattern.matches(lookupPath)) {
559+
return entry.getKey();
560+
}
561+
}
562+
}
563+
return null;
564+
}
565+
}
566+
567+
568+
/**
569+
* Handler registry that selects the handler mapped to the best-matching
570+
* (i.e. most specific) path pattern.
571+
*/
572+
private static final class OrderedHandlerRegistry extends AbstractHandlerRegistry {
573+
574+
private final Map<PathPattern, Handler> handlerMap = new TreeMap<>();
575+
576+
@Override
577+
protected void registerHandlerInternal(PathPattern pathPattern, Handler handler) {
578+
if (this.handlerMap.containsKey(pathPattern)) {
579+
Handler existingHandler = this.handlerMap.get(pathPattern);
580+
if (existingHandler != null && existingHandler != handler) {
581+
throw new IllegalStateException(
582+
"Cannot map " + handler + " to [" + pathPattern + "]: there is already " +
583+
existingHandler + " mapped.");
584+
}
585+
}
586+
this.handlerMap.put(pathPattern, handler);
587+
}
588+
589+
@Override
590+
public @Nullable Handler lookupHandler(RequestPath path, PathContainer lookupPath, HttpServletRequest request) {
591+
for (Map.Entry<PathPattern, Handler> entry : this.handlerMap.entrySet()) {
592+
if (!entry.getKey().matches(lookupPath)) {
593+
continue;
594+
}
595+
if (entry.getValue().supports(request, path)) {
596+
return entry.getValue();
597+
}
598+
return null; // only match one path pattern
599+
}
600+
return null;
601+
}
602+
}
603+
408604
}

0 commit comments

Comments
 (0)