1818
1919import java .io .IOException ;
2020import java .util .Arrays ;
21+ import java .util .Collections ;
2122import java .util .List ;
2223import java .util .Map ;
24+ import java .util .TreeMap ;
2325import java .util .function .Consumer ;
2426
2527import jakarta .servlet .DispatcherType ;
3941import org .springframework .util .Assert ;
4042import org .springframework .util .LinkedMultiValueMap ;
4143import org .springframework .util .MultiValueMap ;
44+ import org .springframework .util .ObjectUtils ;
4245import org .springframework .util .StringUtils ;
4346import org .springframework .web .util .ServletRequestPathUtils ;
4447import org .springframework .web .util .pattern .PathPattern ;
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 */
6670public 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/*"</code>, <code>"/path/**"</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/*"</code>, <code>"/path/**"</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/*"</code>, <code>"/path/foo/**"</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