Skip to content

Commit 02c6e7f

Browse files
committed
Add resource redirection to WebFlux functional router
See spring-projectsgh-27257
1 parent 052bbcc commit 02c6e7f

File tree

8 files changed

+216
-30
lines changed

8 files changed

+216
-30
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.reactive.function.server;
18+
19+
import java.util.function.Function;
20+
21+
import reactor.core.publisher.Mono;
22+
23+
import org.springframework.core.io.Resource;
24+
import org.springframework.util.Assert;
25+
26+
/**
27+
* Lookup function used by {@link RouterFunctions#resource(RequestPredicate, Resource)} and
28+
* {@link RouterFunctions#resource(RequestPredicate, Resource, java.util.function.BiConsumer)}.
29+
*
30+
* @author Sebastien Deleuze
31+
* @since 6.1.4
32+
*/
33+
class PredicateResourceLookupFunction implements Function<ServerRequest, Mono<Resource>> {
34+
35+
private final RequestPredicate predicate;
36+
37+
private final Resource resource;
38+
39+
public PredicateResourceLookupFunction(RequestPredicate predicate, Resource resource) {
40+
Assert.notNull(predicate, "'predicate' must not be null");
41+
Assert.notNull(resource, "'resource' must not be null");
42+
this.predicate = predicate;
43+
this.resource = resource;
44+
}
45+
46+
@Override
47+
public Mono<Resource> apply(ServerRequest serverRequest) {
48+
return this.predicate.test(serverRequest) ? Mono.just(this.resource) : Mono.empty();
49+
}
50+
51+
}

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctionBuilder.java

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -39,6 +39,7 @@
3939
* Default implementation of {@link RouterFunctions.Builder}.
4040
*
4141
* @author Arjen Poutsma
42+
* @author Sebastien Deleuze
4243
* @since 5.1
4344
*/
4445
class RouterFunctionBuilder implements RouterFunctions.Builder {
@@ -238,6 +239,17 @@ public RouterFunctions.Builder route(RequestPredicate predicate,
238239
return add(RouterFunctions.route(predicate, handlerFunction));
239240
}
240241

242+
@Override
243+
public RouterFunctions.Builder resource(RequestPredicate predicate, Resource resource) {
244+
return add(RouterFunctions.resource(predicate, resource));
245+
}
246+
247+
@Override
248+
public RouterFunctions.Builder resource(RequestPredicate predicate, Resource resource,
249+
BiConsumer<Resource, HttpHeaders> headersConsumer) {
250+
return add(RouterFunctions.resource(predicate, resource, headersConsumer));
251+
}
252+
241253
@Override
242254
public RouterFunctions.Builder resources(String pattern, Resource location) {
243255
return add(RouterFunctions.resources(pattern, location));

spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RouterFunctions.java

+65-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -59,6 +59,7 @@
5959
* environments, Reactor, or Undertow.
6060
*
6161
* @author Arjen Poutsma
62+
* @author Sebastien Deleuze
6263
* @since 5.0
6364
*/
6465
public abstract class RouterFunctions {
@@ -145,6 +146,40 @@ public static <T extends ServerResponse> RouterFunction<T> nest(
145146
return new DefaultNestedRouterFunction<>(predicate, routerFunction);
146147
}
147148

149+
/**
150+
* Route requests that match the given predicate to the given resource.
151+
* For instance
152+
* <pre class="code">
153+
* Resource resource = new ClassPathResource("static/index.html")
154+
* RouterFunction&lt;ServerResponse&gt; resources = RouterFunctions.resource(path("/api/**").negate(), resource);
155+
* </pre>
156+
* @param predicate predicate to match
157+
* @param resource the resources to serve
158+
* @return a router function that routes to a resource
159+
* @since 6.1.4
160+
*/
161+
public static RouterFunction<ServerResponse> resource(RequestPredicate predicate, Resource resource) {
162+
return resources(new PredicateResourceLookupFunction(predicate, resource), (consumerResource, httpHeaders) -> {});
163+
}
164+
165+
/**
166+
* Route requests that match the given predicate to the given resource.
167+
* For instance
168+
* <pre class="code">
169+
* Resource resource = new ClassPathResource("static/index.html")
170+
* RouterFunction&lt;ServerResponse&gt; resources = RouterFunctions.resource(path("/api/**").negate(), resource);
171+
* </pre>
172+
* @param predicate predicate to match
173+
* @param resource the resources to serve
174+
* @param headersConsumer provides access to the HTTP headers for served resources
175+
* @return a router function that routes to a resource
176+
* @since 6.1.4
177+
*/
178+
public static RouterFunction<ServerResponse> resource(RequestPredicate predicate, Resource resource,
179+
BiConsumer<Resource, HttpHeaders> headersConsumer) {
180+
return resources(new PredicateResourceLookupFunction(predicate, resource), headersConsumer);
181+
}
182+
148183
/**
149184
* Route requests that match the given pattern to resources relative to the given root location.
150185
* For instance
@@ -692,6 +727,35 @@ public interface Builder {
692727
*/
693728
Builder add(RouterFunction<ServerResponse> routerFunction);
694729

730+
/**
731+
* Route requests that match the given predicate to the given resource.
732+
* For instance
733+
* <pre class="code">
734+
* Resource resource = new ClassPathResource("static/index.html")
735+
* RouterFunction&lt;ServerResponse&gt; resources = RouterFunctions.resource(path("/api/**").negate(), resource);
736+
* </pre>
737+
* @param predicate predicate to match
738+
* @param resource the resources to serve
739+
* @return a router function that routes to a resource
740+
* @since 6.1.4
741+
*/
742+
Builder resource(RequestPredicate predicate, Resource resource);
743+
744+
/**
745+
* Route requests that match the given predicate to the given resource.
746+
* For instance
747+
* <pre class="code">
748+
* Resource resource = new ClassPathResource("static/index.html")
749+
* RouterFunction&lt;ServerResponse&gt; resources = RouterFunctions.resource(path("/api/**").negate(), resource);
750+
* </pre>
751+
* @param predicate predicate to match
752+
* @param resource the resources to serve
753+
* @param headersConsumer provides access to the HTTP headers for served resources
754+
* @return a router function that routes to a resource
755+
* @since 6.1.4
756+
*/
757+
Builder resource(RequestPredicate predicate, Resource resource, BiConsumer<Resource, HttpHeaders> headersConsumer);
758+
695759
/**
696760
* Route requests that match the given pattern to resources relative to the given root location.
697761
* For instance

spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDsl.kt

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import kotlinx.coroutines.reactor.awaitSingle
2323
import kotlinx.coroutines.reactor.mono
2424
import kotlinx.coroutines.withContext
2525
import org.springframework.core.io.Resource
26+
import org.springframework.http.HttpHeaders
2627
import org.springframework.http.HttpMethod
2728
import org.springframework.http.HttpStatusCode
2829
import org.springframework.http.MediaType
@@ -499,6 +500,15 @@ class CoRouterFunctionDsl internal constructor (private val init: (CoRouterFunct
499500
builder.add(RouterFunctions.route(RequestPredicates.path(this), asHandlerFunction(f)))
500501
}
501502

503+
/**
504+
* Route requests that match the given predicate to the given resource.
505+
* @since 6.1.4
506+
* @see RouterFunctions.resource
507+
*/
508+
fun resource(predicate: RequestPredicate, resource: Resource, headersConsumer: (Resource, HttpHeaders) -> Unit = { _, _ -> }) {
509+
builder.resource(predicate, resource, headersConsumer)
510+
}
511+
502512
/**
503513
* Route requests that match the given pattern to resources relative to the given root location.
504514
* @see RouterFunctions.resources

spring-webflux/src/main/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDsl.kt

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -617,6 +617,15 @@ class RouterFunctionDsl internal constructor (private val init: RouterFunctionDs
617617
builder.add(RouterFunctions.route(RequestPredicates.path(this), HandlerFunction { f(it).cast(ServerResponse::class.java) }))
618618
}
619619

620+
/**
621+
* Route requests that match the given predicate to the given resource.
622+
* @since 6.1.4
623+
* @see RouterFunctions.resource
624+
*/
625+
fun resource(predicate: RequestPredicate, resource: Resource) {
626+
builder.resource(predicate, resource)
627+
}
628+
620629
/**
621630
* Route requests that match the given pattern to resources relative to the given root location.
622631
* @see RouterFunctions.resources

spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RouterFunctionBuilderTests.java

+22
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
import static org.assertj.core.api.Assertions.assertThat;
4141
import static org.springframework.web.reactive.function.server.RequestPredicates.HEAD;
42+
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
4243

4344
/**
4445
* @author Arjen Poutsma
@@ -102,6 +103,27 @@ void route() {
102103

103104
}
104105

106+
@Test
107+
void resource() {
108+
Resource resource = new ClassPathResource("/org/springframework/web/reactive/function/server/response.txt");
109+
assertThat(resource.exists()).isTrue();
110+
111+
RouterFunction<ServerResponse> route = RouterFunctions.route()
112+
.resource(path("/test"), resource)
113+
.build();
114+
115+
MockServerHttpRequest mockRequest = MockServerHttpRequest.get("https://localhost/test").build();
116+
ServerRequest resourceRequest = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList());
117+
118+
Mono<HttpStatusCode> responseMono = route.route(resourceRequest)
119+
.flatMap(handlerFunction -> handlerFunction.handle(resourceRequest))
120+
.map(ServerResponse::statusCode);
121+
122+
StepVerifier.create(responseMono)
123+
.expectNext(HttpStatus.OK)
124+
.verifyComplete();
125+
}
126+
105127
@Test
106128
void resources() {
107129
Resource resource = new ClassPathResource("/org/springframework/web/reactive/function/server/");

spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/CoRouterFunctionDslTests.kt

+22-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -93,16 +93,6 @@ class CoRouterFunctionDslTests {
9393
.verifyComplete()
9494
}
9595

96-
@Test
97-
fun resourceByPath() {
98-
val mockRequest = get("https://example.com/org/springframework/web/reactive/function/response.txt")
99-
.build()
100-
val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList())
101-
StepVerifier.create(sampleRouter().route(request))
102-
.expectNextCount(1)
103-
.verifyComplete()
104-
}
105-
10696
@Test
10797
fun method() {
10898
val mockRequest = patch("https://example.com/")
@@ -124,6 +114,24 @@ class CoRouterFunctionDslTests {
124114

125115
@Test
126116
fun resource() {
117+
val mockRequest = get("https://example.com/response2.txt").build()
118+
val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList())
119+
StepVerifier.create(sampleRouter().route(request))
120+
.expectNextCount(1)
121+
.verifyComplete()
122+
}
123+
124+
@Test
125+
fun resources() {
126+
val mockRequest = get("https://example.com/resources/response.txt").build()
127+
val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList())
128+
StepVerifier.create(sampleRouter().route(request))
129+
.expectNextCount(1)
130+
.verifyComplete()
131+
}
132+
133+
@Test
134+
fun resourcesLookupFunction() {
127135
val mockRequest = get("https://example.com/response.txt").build()
128136
val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList())
129137
StepVerifier.create(sampleRouter().route(request))
@@ -305,8 +313,9 @@ class CoRouterFunctionDslTests {
305313
GET("/api/foo/", ::handle)
306314
}
307315
headers({ it.header("bar").isNotEmpty() }, ::handle)
308-
resources("/org/springframework/web/reactive/function/**",
309-
ClassPathResource("/org/springframework/web/reactive/function/response.txt"))
316+
resource(path("/response2.txt"), ClassPathResource("/org/springframework/web/reactive/function/response.txt"))
317+
resources("/resources/**",
318+
ClassPathResource("/org/springframework/web/reactive/function/"))
310319
resources {
311320
if (it.path() == "/response.txt") {
312321
ClassPathResource("/org/springframework/web/reactive/function/response.txt")

spring-webflux/src/test/kotlin/org/springframework/web/reactive/function/server/RouterFunctionDslTests.kt

+22-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -91,16 +91,6 @@ class RouterFunctionDslTests {
9191
.verifyComplete()
9292
}
9393

94-
@Test
95-
fun resourceByPath() {
96-
val mockRequest = get("https://example.com/org/springframework/web/reactive/function/response.txt")
97-
.build()
98-
val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList())
99-
StepVerifier.create(sampleRouter().route(request))
100-
.expectNextCount(1)
101-
.verifyComplete()
102-
}
103-
10494
@Test
10595
fun method() {
10696
val mockRequest = patch("https://example.com/")
@@ -122,6 +112,24 @@ class RouterFunctionDslTests {
122112

123113
@Test
124114
fun resource() {
115+
val mockRequest = get("https://example.com/response2.txt").build()
116+
val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList())
117+
StepVerifier.create(sampleRouter().route(request))
118+
.expectNextCount(1)
119+
.verifyComplete()
120+
}
121+
122+
@Test
123+
fun resources() {
124+
val mockRequest = get("https://example.com/resources/response.txt").build()
125+
val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList())
126+
StepVerifier.create(sampleRouter().route(request))
127+
.expectNextCount(1)
128+
.verifyComplete()
129+
}
130+
131+
@Test
132+
fun resourcesLookupFunction() {
125133
val mockRequest = get("https://example.com/response.txt").build()
126134
val request = DefaultServerRequest(MockServerWebExchange.from(mockRequest), emptyList())
127135
StepVerifier.create(sampleRouter().route(request))
@@ -237,8 +245,9 @@ class RouterFunctionDslTests {
237245
GET("/api/foo/", ::handle)
238246
}
239247
headers({ it.header("bar").isNotEmpty() }, ::handle)
240-
resources("/org/springframework/web/reactive/function/**",
241-
ClassPathResource("/org/springframework/web/reactive/function/response.txt"))
248+
resource(path("/response2.txt"), ClassPathResource("/org/springframework/web/reactive/function/response.txt"))
249+
resources("/resources/**",
250+
ClassPathResource("/org/springframework/web/reactive/function/"))
242251
resources {
243252
if (it.path() == "/response.txt") {
244253
Mono.just(ClassPathResource("/org/springframework/web/reactive/function/response.txt"))

0 commit comments

Comments
 (0)