From e9d9de5f998b0112d472cadf9bed0d6c0bba87a4 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 1 May 2020 14:00:16 +0200 Subject: [PATCH] RouterFunction honors PathPatternParser in config This commit introduces a way to change the PathPatternParser used in PathPredicates, by way of a ChangePathPatternParserVisitor. This visitor is used by both WebFluxConfigurationSupport and WebMvcConfigurationSupport to make sure the configured parser is used. Closes gh-23236 --- .../config/WebFluxConfigurationSupport.java | 22 +++-- .../ChangePathPatternParserVisitor.java | 82 ++++++++++++++++ .../function/server/RequestPredicates.java | 46 +++++++-- .../function/server/RouterFunctions.java | 23 +++++ .../server/support/RouterFunctionMapping.java | 6 +- .../support/RouterFunctionMappingTests.java | 25 ++++- .../WebMvcConfigurationSupport.java | 8 ++ .../ChangePathPatternParserVisitor.java | 81 +++++++++++++++ .../servlet/function/RequestPredicates.java | 50 ++++++++-- .../web/servlet/function/RouterFunctions.java | 34 ++++++- .../support/RouterFunctionMapping.java | 11 ++- .../support/RouterFunctionMappingTests.java | 98 +++++++++++++++++++ 12 files changed, 454 insertions(+), 32 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ChangePathPatternParserVisitor.java create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/function/ChangePathPatternParserVisitor.java create mode 100644 spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/RouterFunctionMappingTests.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java index e7061ce425..e61749bb30 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/config/WebFluxConfigurationSupport.java @@ -128,9 +128,18 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { RequestMappingHandlerMapping mapping = createRequestMappingHandlerMapping(); mapping.setOrder(0); mapping.setContentTypeResolver(contentTypeResolver); - mapping.setCorsConfigurations(getCorsConfigurations()); - PathMatchConfigurer configurer = getPathMatchConfigurer(); + configureAbstractHandlerMapping(mapping, configurer); + Map>> pathPrefixes = configurer.getPathPrefixes(); + if (pathPrefixes != null) { + mapping.setPathPrefixes(pathPrefixes); + } + + return mapping; + } + + private void configureAbstractHandlerMapping(AbstractHandlerMapping mapping, PathMatchConfigurer configurer) { + mapping.setCorsConfigurations(getCorsConfigurations()); Boolean useTrailingSlashMatch = configurer.isUseTrailingSlashMatch(); if (useTrailingSlashMatch != null) { mapping.setUseTrailingSlashMatch(useTrailingSlashMatch); @@ -139,12 +148,6 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { if (useCaseSensitiveMatch != null) { mapping.setUseCaseSensitiveMatch(useCaseSensitiveMatch); } - Map>> pathPrefixes = configurer.getPathPrefixes(); - if (pathPrefixes != null) { - mapping.setPathPrefixes(pathPrefixes); - } - - return mapping; } /** @@ -210,8 +213,7 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { RouterFunctionMapping mapping = createRouterFunctionMapping(); mapping.setOrder(-1); // go before RequestMappingHandlerMapping mapping.setMessageReaders(serverCodecConfigurer.getReaders()); - mapping.setCorsConfigurations(getCorsConfigurations()); - + configureAbstractHandlerMapping(mapping, getPathMatchConfigurer()); return mapping; } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ChangePathPatternParserVisitor.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ChangePathPatternParserVisitor.java new file mode 100644 index 0000000000..32668397f4 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ChangePathPatternParserVisitor.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.reactive.function.server; + +import java.util.function.Function; + +import reactor.core.publisher.Mono; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * Implementation of {@link RouterFunctions.Visitor} that changes the + * {@link PathPatternParser} on path-related request predicates + * (i.e. {@code RequestPredicates.PathPatternPredicate}. + * + * @author Arjen Poutsma + * @since 5.3 + */ +class ChangePathPatternParserVisitor implements RouterFunctions.Visitor { + + private final PathPatternParser parser; + + + public ChangePathPatternParserVisitor(PathPatternParser parser) { + Assert.notNull(parser, "Parser must not be null"); + this.parser = parser; + } + + @Override + public void startNested(RequestPredicate predicate) { + changeParser(predicate); + } + + @Override + public void endNested(RequestPredicate predicate) { + } + + @Override + public void route(RequestPredicate predicate, HandlerFunction handlerFunction) { + changeParser(predicate); + } + + @Override + public void resources(Function> lookupFunction) { + } + + @Override + public void unknown(RouterFunction routerFunction) { + } + + private void changeParser(RequestPredicate predicate) { + if (predicate instanceof Target) { + Target target = (Target) predicate; + target.changeParser(this.parser); + } + } + + + /** + * Interface implemented by predicates that can change the parser. + */ + public interface Target { + + void changeParser(PathPatternParser parser); + } +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java index 006c8fea46..b7b4174a49 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java @@ -441,7 +441,6 @@ public abstract class RequestPredicates { public HttpMethodPredicate(HttpMethod... httpMethods) { Assert.notEmpty(httpMethods, "HttpMethods must not be empty"); - this.httpMethods = EnumSet.copyOf(Arrays.asList(httpMethods)); } @@ -465,7 +464,6 @@ public abstract class RequestPredicates { } } - @Override public void accept(Visitor visitor) { visitor.method(Collections.unmodifiableSet(this.httpMethods)); @@ -483,9 +481,9 @@ public abstract class RequestPredicates { } - private static class PathPatternPredicate implements RequestPredicate { + private static class PathPatternPredicate implements RequestPredicate, ChangePathPatternParserVisitor.Target { - private final PathPattern pattern; + private PathPattern pattern; public PathPatternPredicate(PathPattern pattern) { Assert.notNull(pattern, "'pattern' must not be null"); @@ -529,10 +527,17 @@ public abstract class RequestPredicates { visitor.path(this.pattern.getPatternString()); } + @Override + public void changeParser(PathPatternParser parser) { + String patternString = this.pattern.getPatternString(); + this.pattern = parser.parse(patternString); + } + @Override public String toString() { return this.pattern.getPatternString(); } + } @@ -750,7 +755,7 @@ public abstract class RequestPredicates { * {@link RequestPredicate} for where both {@code left} and {@code right} predicates * must match. */ - static class AndRequestPredicate implements RequestPredicate { + static class AndRequestPredicate implements RequestPredicate, ChangePathPatternParserVisitor.Target { private final RequestPredicate left; @@ -788,6 +793,16 @@ public abstract class RequestPredicates { visitor.endAnd(); } + @Override + public void changeParser(PathPatternParser parser) { + if (this.left instanceof ChangePathPatternParserVisitor.Target) { + ((ChangePathPatternParserVisitor.Target) left).changeParser(parser); + } + if (this.right instanceof ChangePathPatternParserVisitor.Target) { + ((ChangePathPatternParserVisitor.Target) right).changeParser(parser); + } + } + @Override public String toString() { return String.format("(%s && %s)", this.left, this.right); @@ -797,7 +812,8 @@ public abstract class RequestPredicates { /** * {@link RequestPredicate} that negates a delegate predicate. */ - static class NegateRequestPredicate implements RequestPredicate { + static class NegateRequestPredicate implements RequestPredicate, ChangePathPatternParserVisitor.Target { + private final RequestPredicate delegate; public NegateRequestPredicate(RequestPredicate delegate) { @@ -822,6 +838,13 @@ public abstract class RequestPredicates { visitor.endNegate(); } + @Override + public void changeParser(PathPatternParser parser) { + if (this.delegate instanceof ChangePathPatternParserVisitor.Target) { + ((ChangePathPatternParserVisitor.Target) delegate).changeParser(parser); + } + } + @Override public String toString() { return "!" + this.delegate.toString(); @@ -832,7 +855,7 @@ public abstract class RequestPredicates { * {@link RequestPredicate} where either {@code left} or {@code right} predicates * may match. */ - static class OrRequestPredicate implements RequestPredicate { + static class OrRequestPredicate implements RequestPredicate, ChangePathPatternParserVisitor.Target { private final RequestPredicate left; @@ -882,6 +905,15 @@ public abstract class RequestPredicates { visitor.endOr(); } + @Override + public void changeParser(PathPatternParser parser) { + if (this.left instanceof ChangePathPatternParserVisitor.Target) { + ((ChangePathPatternParserVisitor.Target) left).changeParser(parser); + } + if (this.right instanceof ChangePathPatternParserVisitor.Target) { + ((ChangePathPatternParserVisitor.Target) right).changeParser(parser); + } + } @Override public String toString() { diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java index cd777c38db..8106092cbc 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java @@ -37,6 +37,7 @@ import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebHandler; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; +import org.springframework.web.util.pattern.PathPatternParser; /** * Central entry point to Spring's functional web framework. @@ -255,6 +256,26 @@ public abstract class RouterFunctions { return new RouterFunctionWebHandler(strategies, routerFunction); } + /** + * Changes the {@link PathPatternParser} on the given {@linkplain RouterFunction router function}. This method + * can be used to change the {@code PathPatternParser} properties from the defaults, for instance to change + * {@linkplain PathPatternParser#setCaseSensitive(boolean) case sensitivity}. + * @param routerFunction the router function to change the parser in + * @param parser the parser to change to. + * @param the type of response returned by the handler function + * @return the change router function + */ + public static RouterFunction changeParser(RouterFunction routerFunction, + PathPatternParser parser) { + + Assert.notNull(routerFunction, "RouterFunction must not be null"); + Assert.notNull(parser, "Parser must not be null"); + + ChangePathPatternParserVisitor visitor = new ChangePathPatternParserVisitor(parser); + routerFunction.accept(visitor); + return routerFunction; + } + /** * Represents a discoverable builder for router functions. * Obtained via {@link RouterFunctions#route()}. @@ -895,6 +916,7 @@ public abstract class RouterFunctions { public void accept(Visitor visitor) { visitor.route(this.predicate, this.handlerFunction); } + } @@ -938,6 +960,7 @@ public abstract class RouterFunctions { this.routerFunction.accept(visitor); visitor.endNested(this.predicate); } + } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/RouterFunctionMapping.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/RouterFunctionMapping.java index 33baed7749..5d25ee817e 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/RouterFunctionMapping.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/support/RouterFunctionMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -103,6 +103,10 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini if (this.routerFunction == null) { initRouterFunctions(); } + if (this.routerFunction != null) { + RouterFunctions.changeParser(this.routerFunction, getPathPatternParser()); + } + } /** diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/RouterFunctionMappingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/RouterFunctionMappingTests.java index 4daa40f150..a3f7cfdf9d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/RouterFunctionMappingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/support/RouterFunctionMappingTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import reactor.test.StepVerifier; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; @@ -67,4 +68,26 @@ public class RouterFunctionMappingTests { .verify(); } + @Test + public void changeParser() throws Exception { + HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); + RouterFunction routerFunction = RouterFunctions.route() + .GET("/foo", handlerFunction) + .POST("/bar", handlerFunction) + .build(); + + RouterFunctionMapping mapping = new RouterFunctionMapping(routerFunction); + mapping.setMessageReaders(this.codecConfigurer.getReaders()); + mapping.setUseCaseSensitiveMatch(false); + mapping.afterPropertiesSet(); + + ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("https://example.com/FOO")); + Mono result = mapping.getHandler(exchange); + + StepVerifier.create(result) + .expectNext(handlerFunction) + .verifyComplete(); + + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index b85cd5cdb3..5bcdbe4ae5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -109,6 +109,7 @@ import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator; import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.springframework.web.servlet.view.ViewResolverComposite; import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.pattern.PathPatternParser; /** * This is the main class providing the configuration behind the MVC Java config. @@ -519,6 +520,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv *
  • {@link #addInterceptors} for adding handler interceptors. *
  • {@link #addCorsMappings} to configure cross origin requests processing. *
  • {@link #configureMessageConverters} for adding custom message converters. + *
  • {@link #configurePathMatch(PathMatchConfigurer)} for customizing the {@link PathPatternParser}. * * @since 5.2 */ @@ -532,6 +534,12 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv mapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider)); mapping.setCorsConfigurations(getCorsConfigurations()); mapping.setMessageConverters(getMessageConverters()); + + PathPatternParser patternParser = getPathMatchConfigurer().getPatternParser(); + if (patternParser != null) { + mapping.setPatternParser(patternParser); + } + return mapping; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ChangePathPatternParserVisitor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ChangePathPatternParserVisitor.java new file mode 100644 index 0000000000..3a360f2fd5 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/ChangePathPatternParserVisitor.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.function; + +import java.util.Optional; +import java.util.function.Function; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * Implementation of {@link RouterFunctions.Visitor} that changes the + * {@link PathPatternParser} on path-related request predicates + * (i.e. {@code RequestPredicates.PathPatternPredicate}. + * + * @author Arjen Poutsma + * @since 5.3 + */ +class ChangePathPatternParserVisitor implements RouterFunctions.Visitor { + + private final PathPatternParser parser; + + + public ChangePathPatternParserVisitor(PathPatternParser parser) { + Assert.notNull(parser, "Parser must not be null"); + this.parser = parser; + } + + @Override + public void startNested(RequestPredicate predicate) { + changeParser(predicate); + } + + @Override + public void endNested(RequestPredicate predicate) { + } + + @Override + public void route(RequestPredicate predicate, HandlerFunction handlerFunction) { + changeParser(predicate); + } + + @Override + public void resources(Function> lookupFunction) { + } + + @Override + public void unknown(RouterFunction routerFunction) { + } + + private void changeParser(RequestPredicate predicate) { + if (predicate instanceof Target) { + Target target = (Target) predicate; + target.changeParser(this.parser); + } + } + + + /** + * Interface implemented by predicates that can change the parser. + */ + public interface Target { + + void changeParser(PathPatternParser parser); + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java index 30ceb99dd4..0b33c63b8d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java @@ -427,11 +427,11 @@ public abstract class RequestPredicates { void unknown(RequestPredicate predicate); } + private static class HttpMethodPredicate implements RequestPredicate { private final Set httpMethods; - public HttpMethodPredicate(HttpMethod httpMethod) { Assert.notNull(httpMethod, "HttpMethod must not be null"); this.httpMethods = EnumSet.of(httpMethod); @@ -479,9 +479,9 @@ public abstract class RequestPredicates { } - private static class PathPatternPredicate implements RequestPredicate { + private static class PathPatternPredicate implements RequestPredicate, ChangePathPatternParserVisitor.Target { - private final PathPattern pattern; + private PathPattern pattern; public PathPatternPredicate(PathPattern pattern) { Assert.notNull(pattern, "'pattern' must not be null"); @@ -513,6 +513,7 @@ public abstract class RequestPredicates { pattern); request.attributes().put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, pattern); } + @Override public Optional nest(ServerRequest request) { return Optional.ofNullable(this.pattern.matchStartOfPath(request.pathContainer())) @@ -524,10 +525,17 @@ public abstract class RequestPredicates { visitor.path(this.pattern.getPatternString()); } + @Override + public void changeParser(PathPatternParser parser) { + String patternString = this.pattern.getPatternString(); + this.pattern = parser.parse(patternString); + } + @Override public String toString() { return this.pattern.getPatternString(); } + } @@ -642,12 +650,14 @@ public abstract class RequestPredicates { } } + private static class PathExtensionPredicate implements RequestPredicate { private final Predicate extensionPredicate; @Nullable private final String extension; + public PathExtensionPredicate(Predicate extensionPredicate) { Assert.notNull(extensionPredicate, "Predicate must not be null"); this.extensionPredicate = extensionPredicate; @@ -743,7 +753,7 @@ public abstract class RequestPredicates { * {@link RequestPredicate} for where both {@code left} and {@code right} predicates * must match. */ - static class AndRequestPredicate implements RequestPredicate { + static class AndRequestPredicate implements RequestPredicate, ChangePathPatternParserVisitor.Target { private final RequestPredicate left; @@ -781,6 +791,16 @@ public abstract class RequestPredicates { visitor.endAnd(); } + @Override + public void changeParser(PathPatternParser parser) { + if (this.left instanceof ChangePathPatternParserVisitor.Target) { + ((ChangePathPatternParserVisitor.Target) left).changeParser(parser); + } + if (this.right instanceof ChangePathPatternParserVisitor.Target) { + ((ChangePathPatternParserVisitor.Target) right).changeParser(parser); + } + } + @Override public String toString() { return String.format("(%s && %s)", this.left, this.right); @@ -790,7 +810,8 @@ public abstract class RequestPredicates { /** * {@link RequestPredicate} that negates a delegate predicate. */ - static class NegateRequestPredicate implements RequestPredicate { + static class NegateRequestPredicate implements RequestPredicate, ChangePathPatternParserVisitor.Target { + private final RequestPredicate delegate; public NegateRequestPredicate(RequestPredicate delegate) { @@ -815,6 +836,13 @@ public abstract class RequestPredicates { visitor.endNegate(); } + @Override + public void changeParser(PathPatternParser parser) { + if (this.delegate instanceof ChangePathPatternParserVisitor.Target) { + ((ChangePathPatternParserVisitor.Target) delegate).changeParser(parser); + } + } + @Override public String toString() { return "!" + this.delegate.toString(); @@ -825,7 +853,7 @@ public abstract class RequestPredicates { * {@link RequestPredicate} where either {@code left} or {@code right} predicates * may match. */ - static class OrRequestPredicate implements RequestPredicate { + static class OrRequestPredicate implements RequestPredicate, ChangePathPatternParserVisitor.Target { private final RequestPredicate left; @@ -875,6 +903,16 @@ public abstract class RequestPredicates { visitor.endOr(); } + @Override + public void changeParser(PathPatternParser parser) { + if (this.left instanceof ChangePathPatternParserVisitor.Target) { + ((ChangePathPatternParserVisitor.Target) left).changeParser(parser); + } + if (this.right instanceof ChangePathPatternParserVisitor.Target) { + ((ChangePathPatternParserVisitor.Target) right).changeParser(parser); + } + } + @Override public String toString() { return String.format("(%s || %s)", this.left, this.right); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java index 9122b43629..8a9904b191 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java @@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.io.Resource; import org.springframework.util.Assert; +import org.springframework.web.util.pattern.PathPatternParser; /** * Central entry point to Spring's functional web framework. @@ -168,6 +169,26 @@ public abstract class RouterFunctions { return new ResourcesRouterFunction(lookupFunction); } + /** + * Changes the {@link PathPatternParser} on the given {@linkplain RouterFunction router function}. This method + * can be used to change the {@code PathPatternParser} properties from the defaults, for instance to change + * {@linkplain PathPatternParser#setCaseSensitive(boolean) case sensitivity}. + * @param routerFunction the router function to change the parser in + * @param parser the parser to change to. + * @param the type of response returned by the handler function + * @return the change router function + */ + public static RouterFunction changeParser(RouterFunction routerFunction, + PathPatternParser parser) { + + Assert.notNull(routerFunction, "RouterFunction must not be null"); + Assert.notNull(parser, "Parser must not be null"); + + ChangePathPatternParserVisitor visitor = new ChangePathPatternParserVisitor(parser); + routerFunction.accept(visitor); + return routerFunction; + } + /** * Represents a discoverable builder for router functions. @@ -617,6 +638,8 @@ public abstract class RouterFunctions { */ RouterFunction build(); } + + /** * Receives notifications from the logical structure of router functions. */ @@ -670,6 +693,7 @@ public abstract class RouterFunctions { } } + /** * A composed routing function that first invokes one function, and then invokes the * another function (of the same response type {@code T}) if this route had @@ -705,6 +729,7 @@ public abstract class RouterFunctions { } } + /** * A composed routing function that first invokes one function, and then invokes * another function (of a different response type) if this route had @@ -778,8 +803,8 @@ public abstract class RouterFunctions { } } - private static final class DefaultRouterFunction - extends AbstractRouterFunction { + + private static final class DefaultRouterFunction extends AbstractRouterFunction { private final RequestPredicate predicate; @@ -812,8 +837,8 @@ public abstract class RouterFunctions { } - private static final class DefaultNestedRouterFunction - extends AbstractRouterFunction { + + private static final class DefaultNestedRouterFunction extends AbstractRouterFunction { private final RequestPredicate predicate; @@ -858,6 +883,7 @@ public abstract class RouterFunctions { } + private static class ResourcesRouterFunction extends AbstractRouterFunction { private final Function> lookupFunction; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java index 1cfed94e2c..3a5cc38080 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/support/RouterFunctionMapping.java @@ -77,9 +77,6 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini * {@link RouterFunction} instances available in the application context. */ public RouterFunctionMapping() { - // gh-23236 will ensure the configured parser is used to parse patterns - // For now this helps to signal to the DispatcherServlet the need to initialize the RequestPath - setPatternParser(new PathPatternParser()); } /** @@ -135,6 +132,14 @@ public class RouterFunctionMapping extends AbstractHandlerMapping implements Ini if (CollectionUtils.isEmpty(this.messageConverters)) { initMessageConverters(); } + if (this.routerFunction != null) { + PathPatternParser patternParser = getPatternParser(); + if (patternParser == null) { + patternParser = new PathPatternParser(); + setPatternParser(patternParser); + } + RouterFunctions.changeParser(this.routerFunction, patternParser); + } } /** diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/RouterFunctionMappingTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/RouterFunctionMappingTests.java new file mode 100644 index 0000000000..72a5c528f9 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/support/RouterFunctionMappingTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.function.support; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.util.ServletRequestPathUtils; +import org.springframework.web.util.pattern.PathPatternParser; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class RouterFunctionMappingTests { + + private final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/match"); + + private List> messageConverters = Collections.emptyList(); + + @Test + public void normal() throws Exception { + HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); + RouterFunction routerFunction = request -> Optional.of(handlerFunction); + + RouterFunctionMapping mapping = new RouterFunctionMapping(routerFunction); + mapping.setMessageConverters(this.messageConverters); + ServletRequestPathUtils.parseAndCache(this.request); + + HandlerExecutionChain result = mapping.getHandler(this.request); + + assertThat(result).isNotNull(); + assertThat(result.getHandler()).isSameAs(handlerFunction); + } + + @Test + public void noMatch() throws Exception { + RouterFunction routerFunction = request -> Optional.empty(); + + RouterFunctionMapping mapping = new RouterFunctionMapping(routerFunction); + mapping.setMessageConverters(this.messageConverters); + ServletRequestPathUtils.parseAndCache(this.request); + + HandlerExecutionChain result = mapping.getHandler(this.request); + + assertThat(result).isNull(); + } + + @Test + public void changeParser() throws Exception { + HandlerFunction handlerFunction = request -> ServerResponse.ok().build(); + RouterFunction routerFunction = RouterFunctions.route() + .GET("/foo", handlerFunction) + .POST("/bar", handlerFunction) + .build(); + + RouterFunctionMapping mapping = new RouterFunctionMapping(routerFunction); + mapping.setMessageConverters(this.messageConverters); + PathPatternParser patternParser = new PathPatternParser(); + patternParser.setCaseSensitive(false); + mapping.setPatternParser(patternParser); + mapping.afterPropertiesSet(); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/FOO"); + ServletRequestPathUtils.parseAndCache(request); + + HandlerExecutionChain result = mapping.getHandler(request); + + assertThat(result).isNotNull(); + assertThat(result.getHandler()).isSameAs(handlerFunction); + } + +}