From d3ca6f9f6a881b1bf88c92592835b655b1ee0ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 9 Feb 2024 11:16:15 +0100 Subject: [PATCH] Add resource redirection to WebMVC functional router See gh-27257 --- .../PredicateResourceLookupFunction.java | 50 ++++++++++++++ .../function/RouterFunctionBuilder.java | 14 +++- .../web/servlet/function/RouterFunctions.java | 66 ++++++++++++++++++- .../web/servlet/function/RouterFunctionDsl.kt | 11 +++- .../function/RouterFunctionBuilderTests.java | 19 ++++++ .../function/RouterFunctionDslTests.kt | 29 ++++---- 6 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 spring-webmvc/src/main/java/org/springframework/web/servlet/function/PredicateResourceLookupFunction.java diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/PredicateResourceLookupFunction.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/PredicateResourceLookupFunction.java new file mode 100644 index 0000000000..0958bdf7ed --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/PredicateResourceLookupFunction.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2024 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; + +/** + * Lookup function used by {@link RouterFunctions#resource(RequestPredicate, Resource)} and + * {@link RouterFunctions#resource(RequestPredicate, Resource, java.util.function.BiConsumer)}. + * + * @author Sebastien Deleuze + * @since 6.1.4 + */ +class PredicateResourceLookupFunction implements Function> { + + private final RequestPredicate predicate; + + private final Resource resource; + + public PredicateResourceLookupFunction(RequestPredicate predicate, Resource resource) { + Assert.notNull(predicate, "'predicate' must not be null"); + Assert.notNull(resource, "'resource' must not be null"); + this.predicate = predicate; + this.resource = resource; + } + + @Override + public Optional apply(ServerRequest serverRequest) { + return this.predicate.test(serverRequest) ? Optional.of(this.resource) : Optional.empty(); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java index 0a845ec8b4..6a5c4806b7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctionBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -37,6 +37,7 @@ import org.springframework.util.Assert; * Default implementation of {@link RouterFunctions.Builder}. * * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 5.2 */ class RouterFunctionBuilder implements RouterFunctions.Builder { @@ -236,6 +237,17 @@ class RouterFunctionBuilder implements RouterFunctions.Builder { return add(RouterFunctions.route(predicate, handlerFunction)); } + @Override + public RouterFunctions.Builder resource(RequestPredicate predicate, Resource resource) { + return add(RouterFunctions.resource(predicate, resource)); + } + + @Override + public RouterFunctions.Builder resource(RequestPredicate predicate, Resource resource, + BiConsumer headersConsumer) { + return add(RouterFunctions.resource(predicate, resource, headersConsumer)); + } + @Override public RouterFunctions.Builder resources(String pattern, Resource location) { return add(RouterFunctions.resources(pattern, location)); 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 6693950b23..7e8a42004f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -45,6 +45,7 @@ import org.springframework.web.util.pattern.PathPatternParser; * function. * * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 5.2 */ public abstract class RouterFunctions { @@ -128,6 +129,40 @@ public abstract class RouterFunctions { return new DefaultNestedRouterFunction<>(predicate, routerFunction); } + /** + * Route requests that match the given predicate to the given resource. + * For instance + *
+	 * Resource resource = new ClassPathResource("static/index.html")
+	 * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource);
+	 * 
+ * @param predicate predicate to match + * @param resource the resources to serve + * @return a router function that routes to a resource + * @since 6.1.4 + */ + public static RouterFunction resource(RequestPredicate predicate, Resource resource) { + return resources(new PredicateResourceLookupFunction(predicate, resource), (consumerResource, httpHeaders) -> {}); + } + + /** + * Route requests that match the given predicate to the given resource. + * For instance + *
+	 * Resource resource = new ClassPathResource("static/index.html")
+	 * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource);
+	 * 
+ * @param predicate predicate to match + * @param resource the resources to serve + * @param headersConsumer provides access to the HTTP headers for served resources + * @return a router function that routes to a resource + * @since 6.1.4 + */ + public static RouterFunction resource(RequestPredicate predicate, Resource resource, + BiConsumer headersConsumer) { + return resources(new PredicateResourceLookupFunction(predicate, resource), headersConsumer); + } + /** * Route requests that match the given pattern to resources relative to the given root location. * For instance @@ -602,6 +637,35 @@ public abstract class RouterFunctions { */ Builder add(RouterFunction routerFunction); + /** + * Route requests that match the given predicate to the given resource. + * For instance + *
+		 * Resource resource = new ClassPathResource("static/index.html")
+		 * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource);
+		 * 
+ * @param predicate predicate to match + * @param resource the resources to serve + * @return a router function that routes to a resource + * @since 6.1.4 + */ + Builder resource(RequestPredicate predicate, Resource resource); + + /** + * Route requests that match the given predicate to the given resource. + * For instance + *
+		 * Resource resource = new ClassPathResource("static/index.html")
+		 * RouterFunction<ServerResponse> resources = RouterFunctions.resource(path("/api/**").negate(), resource);
+		 * 
+ * @param predicate predicate to match + * @param resource the resources to serve + * @param headersConsumer provides access to the HTTP headers for served resources + * @return a router function that routes to a resource + * @since 6.1.4 + */ + Builder resource(RequestPredicate predicate, Resource resource, BiConsumer headersConsumer); + /** * Route requests that match the given pattern to resources relative to the given root location. * For instance diff --git a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt index a39add496e..f44553674c 100644 --- a/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt +++ b/spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/RouterFunctionDsl.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -612,6 +612,15 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD builder.add(RouterFunctions.route(RequestPredicates.path(this), HandlerFunction(f))) } + /** + * Route requests that match the given predicate to the given resource. + * @see RouterFunctions.resource + * @since 6.1.4 + */ + fun resource(predicate: RequestPredicate, resource: Resource) { + builder.resource(predicate, resource) + } + /** * Route requests that match the given pattern to resources relative to the given root location. * @see RouterFunctions.resources diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java index 5e3c83baa1..c36270257f 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java @@ -39,11 +39,13 @@ import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.web.servlet.function.RequestPredicates.HEAD; +import static org.springframework.web.servlet.function.RequestPredicates.path; /** * Tests for {@link RouterFunctionBuilder}. * * @author Arjen Poutsma + * @author Sebastien Deleuze */ class RouterFunctionBuilderTests { @@ -96,6 +98,23 @@ class RouterFunctionBuilderTests { } } + @Test + void resource() { + Resource resource = new ClassPathResource("/org/springframework/web/servlet/function/response.txt"); + assertThat(resource.exists()).isTrue(); + + RouterFunction route = RouterFunctions.route() + .resource(path("/test"), resource) + .build(); + + ServerRequest resourceRequest = initRequest("GET", "/test"); + + Optional responseStatus = route.route(resourceRequest) + .map(handlerFunction -> handle(handlerFunction, resourceRequest)) + .map(ServerResponse::statusCode); + assertThat(responseStatus).contains(HttpStatus.OK); + } + @Test void resources() { Resource resource = new ClassPathResource("/org/springframework/web/servlet/function/"); diff --git a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt index ccfb300b39..0a8cdb9d7f 100644 --- a/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt +++ b/spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 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. @@ -74,14 +74,6 @@ class RouterFunctionDslTests { assertThat(sampleRouter().route(request).isPresent).isTrue() } - @Test - fun resourceByPath() { - val servletRequest = PathPatternsTestUtils.initRequest( - "GET", "/org/springframework/web/servlet/function/response.txt", true) - val request = DefaultServerRequest(servletRequest, emptyList()) - assertThat(sampleRouter().route(request).isPresent).isTrue() - } - @Test fun method() { val servletRequest = PathPatternsTestUtils.initRequest("PATCH", "/", true) @@ -98,6 +90,20 @@ class RouterFunctionDslTests { @Test fun resource() { + val servletRequest = PathPatternsTestUtils.initRequest("GET","/response2.txt", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).isPresent).isTrue() + } + + @Test + fun resources() { + val servletRequest = PathPatternsTestUtils.initRequest("GET", "/resources/response.txt", true) + val request = DefaultServerRequest(servletRequest, emptyList()) + assertThat(sampleRouter().route(request).isPresent).isTrue() + } + + @Test + fun resourcesLookupFunction() { val servletRequest = PathPatternsTestUtils.initRequest("GET", "/response.txt", true) val request = DefaultServerRequest(servletRequest, emptyList()) assertThat(sampleRouter().route(request).isPresent).isTrue() @@ -168,8 +174,9 @@ class RouterFunctionDslTests { GET("/api/foo/", ::handle) } headers({ it.header("bar").isNotEmpty() }, ::handle) - resources("/org/springframework/web/servlet/function/**", - ClassPathResource("/org/springframework/web/servlet/function/response.txt")) + resource(path("/response2.txt"), ClassPathResource("/org/springframework/web/servlet/function/response.txt")) + resources("/resources/**", + ClassPathResource("/org/springframework/web/servlet/function/")) resources { if (it.path() == "/response.txt") { ClassPathResource("/org/springframework/web/servlet/function/response.txt")