From 2d7b2e59b62e759c3b8559befa13898f9164aff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Fri, 9 Feb 2024 10:29:32 +0100 Subject: [PATCH] Add resource redirection to WebFlux functional router See gh-27257 --- .../PredicateResourceLookupFunction.java | 51 ++++++++++++++ .../server/RouterFunctionBuilder.java | 14 +++- .../function/server/RouterFunctions.java | 66 ++++++++++++++++++- .../function/server/CoRouterFunctionDsl.kt | 12 +++- .../function/server/RouterFunctionDsl.kt | 11 +++- .../server/RouterFunctionBuilderTests.java | 22 +++++++ .../server/CoRouterFunctionDslTests.kt | 35 ++++++---- .../function/server/RouterFunctionDslTests.kt | 35 ++++++---- 8 files changed, 216 insertions(+), 30 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/server/PredicateResourceLookupFunction.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/PredicateResourceLookupFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/PredicateResourceLookupFunction.java new file mode 100644 index 00000000000..e18dc90267e --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/PredicateResourceLookupFunction.java @@ -0,0 +1,51 @@ +/* + * 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.reactive.function.server; + +import java.util.function.Function; + +import reactor.core.publisher.Mono; + +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 Mono apply(ServerRequest serverRequest) { + return this.predicate.test(serverRequest) ? Mono.just(this.resource) : Mono.empty(); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java index 716566a6de6..e8133b2f5cc 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/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. @@ -39,6 +39,7 @@ import org.springframework.util.Assert; * Default implementation of {@link RouterFunctions.Builder}. * * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 5.1 */ class RouterFunctionBuilder implements RouterFunctions.Builder { @@ -238,6 +239,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-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 e4e55621593..ef350213328 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 @@ -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. @@ -59,6 +59,7 @@ import org.springframework.web.util.pattern.PathPatternParser; * environments, Reactor, or Undertow. * * @author Arjen Poutsma + * @author Sebastien Deleuze * @since 5.0 */ public abstract class RouterFunctions { @@ -145,6 +146,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 @@ -692,6 +727,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-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt index 62febc28f2d..b42f30d8d5c 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.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. @@ -23,6 +23,7 @@ import kotlinx.coroutines.reactor.awaitSingle import kotlinx.coroutines.reactor.mono import kotlinx.coroutines.withContext import org.springframework.core.io.Resource +import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.http.HttpStatusCode import org.springframework.http.MediaType @@ -499,6 +500,15 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct builder.add(RouterFunctions.route(RequestPredicates.path(this), asHandlerFunction(f))) } + /** + * Route requests that match the given predicate to the given resource. + * @since 6.1.4 + * @see RouterFunctions.resource + */ + fun resource(predicate: RequestPredicate, resource: Resource, headersConsumer: (Resource, HttpHeaders) -> Unit = { _, _ -> }) { + builder.resource(predicate, resource, headersConsumer) + } + /** * Route requests that match the given pattern to resources relative to the given root location. * @see RouterFunctions.resources diff --git a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDsl.kt b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDsl.kt index d2c29754963..dea88b9f20f 100644 --- a/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDsl.kt +++ b/spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/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. @@ -617,6 +617,15 @@ class RouterFunctionDsl internal constructor (private val init: RouterFunctionDs builder.add(RouterFunctions.route(RequestPredicates.path(this), HandlerFunction { f(it).cast(ServerResponse::class.java) })) } + /** + * Route requests that match the given predicate to the given resource. + * @since 6.1.4 + * @see RouterFunctions.resource + */ + 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-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionBuilderTests.java index 26edb5fc055..ff1fcb4f258 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionBuilderTests.java @@ -39,6 +39,7 @@ import org.springframework.web.testfixture.server.MockServerWebExchange; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.web.reactive.function.server.RequestPredicates.HEAD; +import static org.springframework.web.reactive.function.server.RequestPredicates.path; /** * @author Arjen Poutsma @@ -102,6 +103,27 @@ class RouterFunctionBuilderTests { } + @Test + void resource() { + Resource resource = new ClassPathResource("/org/springframework/web/reactive/function/server/response.txt"); + assertThat(resource.exists()).isTrue(); + + RouterFunction route = RouterFunctions.route() + .resource(path("/test"), resource) + .build(); + + MockServerHttpRequest mockRequest = MockServerHttpRequest.get("https://localhost/test").build(); + ServerRequest resourceRequest = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); + + Mono responseMono = route.route(resourceRequest) + .flatMap(handlerFunction -> handlerFunction.handle(resourceRequest)) + .map(ServerResponse::statusCode); + + StepVerifier.create(responseMono) + .expectNext(HttpStatus.OK) + .verifyComplete(); + } + @Test void resources() { Resource resource = new ClassPathResource("/org/springframework/web/reactive/function/server/"); diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt index 88f6436a650..a443fcedadc 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.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. @@ -93,16 +93,6 @@ class CoRouterFunctionDslTests { .verifyComplete() } - @Test - fun resourceByPath() { - val mockRequest = get("https://example.com/org/springframework/web/reactive/function/response.txt") - .build() - val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) - StepVerifier.create(sampleRouter().route(request)) - .expectNextCount(1) - .verifyComplete() - } - @Test fun method() { val mockRequest = patch("https://example.com/") @@ -124,6 +114,24 @@ class CoRouterFunctionDslTests { @Test fun resource() { + val mockRequest = get("https://example.com/response2.txt").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun resources() { + val mockRequest = get("https://example.com/resources/response.txt").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun resourcesLookupFunction() { val mockRequest = get("https://example.com/response.txt").build() val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) StepVerifier.create(sampleRouter().route(request)) @@ -305,8 +313,9 @@ class CoRouterFunctionDslTests { GET("/api/foo/", ::handle) } headers({ it.header("bar").isNotEmpty() }, ::handle) - resources("/org/springframework/web/reactive/function/**", - ClassPathResource("/org/springframework/web/reactive/function/response.txt")) + resource(path("/response2.txt"), ClassPathResource("/org/springframework/web/reactive/function/response.txt")) + resources("/resources/**", + ClassPathResource("/org/springframework/web/reactive/function/")) resources { if (it.path() == "/response.txt") { ClassPathResource("/org/springframework/web/reactive/function/response.txt") diff --git a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDslTests.kt b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDslTests.kt index b9b5dab60a1..2bbf77566e4 100644 --- a/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDslTests.kt +++ b/spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDslTests.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. @@ -91,16 +91,6 @@ class RouterFunctionDslTests { .verifyComplete() } - @Test - fun resourceByPath() { - val mockRequest = get("https://example.com/org/springframework/web/reactive/function/response.txt") - .build() - val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) - StepVerifier.create(sampleRouter().route(request)) - .expectNextCount(1) - .verifyComplete() - } - @Test fun method() { val mockRequest = patch("https://example.com/") @@ -122,6 +112,24 @@ class RouterFunctionDslTests { @Test fun resource() { + val mockRequest = get("https://example.com/response2.txt").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun resources() { + val mockRequest = get("https://example.com/resources/response.txt").build() + val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) + StepVerifier.create(sampleRouter().route(request)) + .expectNextCount(1) + .verifyComplete() + } + + @Test + fun resourcesLookupFunction() { val mockRequest = get("https://example.com/response.txt").build() val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList()) StepVerifier.create(sampleRouter().route(request)) @@ -237,8 +245,9 @@ class RouterFunctionDslTests { GET("/api/foo/", ::handle) } headers({ it.header("bar").isNotEmpty() }, ::handle) - resources("/org/springframework/web/reactive/function/**", - ClassPathResource("/org/springframework/web/reactive/function/response.txt")) + resource(path("/response2.txt"), ClassPathResource("/org/springframework/web/reactive/function/response.txt")) + resources("/resources/**", + ClassPathResource("/org/springframework/web/reactive/function/")) resources { if (it.path() == "/response.txt") { Mono.just(ClassPathResource("/org/springframework/web/reactive/function/response.txt"))