Skip to content

Commit 659bbfa

Browse files
committed
Add resource redirection to WebMVC functional router
See spring-projectsgh-27257
1 parent 02c6e7f commit 659bbfa

File tree

6 files changed

+176
-14
lines changed

6 files changed

+176
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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.servlet.function;
18+
19+
import java.util.Optional;
20+
import java.util.function.Function;
21+
22+
import org.springframework.core.io.Resource;
23+
import org.springframework.util.Assert;
24+
25+
/**
26+
* Lookup function used by {@link RouterFunctions#resource(RequestPredicate, Resource)} and
27+
* {@link RouterFunctions#resource(RequestPredicate, Resource, java.util.function.BiConsumer)}.
28+
*
29+
* @author Sebastien Deleuze
30+
* @since 6.1.4
31+
*/
32+
class PredicateResourceLookupFunction implements Function<ServerRequest, Optional<Resource>> {
33+
34+
private final RequestPredicate predicate;
35+
36+
private final Resource resource;
37+
38+
public PredicateResourceLookupFunction(RequestPredicate predicate, Resource resource) {
39+
Assert.notNull(predicate, "'predicate' must not be null");
40+
Assert.notNull(resource, "'resource' must not be null");
41+
this.predicate = predicate;
42+
this.resource = resource;
43+
}
44+
45+
@Override
46+
public Optional<Resource> apply(ServerRequest serverRequest) {
47+
return this.predicate.test(serverRequest) ? Optional.of(this.resource) : Optional.empty();
48+
}
49+
50+
}

spring-webmvc/src/main/java/org/springframework/web/servlet/function/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.
@@ -37,6 +37,7 @@
3737
* Default implementation of {@link RouterFunctions.Builder}.
3838
*
3939
* @author Arjen Poutsma
40+
* @author Sebastien Deleuze
4041
* @since 5.2
4142
*/
4243
class RouterFunctionBuilder implements RouterFunctions.Builder {
@@ -236,6 +237,17 @@ public RouterFunctions.Builder route(RequestPredicate predicate,
236237
return add(RouterFunctions.route(predicate, handlerFunction));
237238
}
238239

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

spring-webmvc/src/main/java/org/springframework/web/servlet/function/RouterFunctions.java

+66-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.
@@ -45,6 +45,7 @@
4545
* function.
4646
*
4747
* @author Arjen Poutsma
48+
* @author Sebastien Deleuze
4849
* @since 5.2
4950
*/
5051
public abstract class RouterFunctions {
@@ -128,6 +129,40 @@ public static <T extends ServerResponse> RouterFunction<T> nest(
128129
return new DefaultNestedRouterFunction<>(predicate, routerFunction);
129130
}
130131

132+
/**
133+
* Route requests that match the given predicate to the given resource.
134+
* For instance
135+
* <pre class="code">
136+
* Resource resource = new ClassPathResource("static/index.html")
137+
* RouterFunction&lt;ServerResponse&gt; resources = RouterFunctions.resource(path("/api/**").negate(), resource);
138+
* </pre>
139+
* @param predicate predicate to match
140+
* @param resource the resources to serve
141+
* @return a router function that routes to a resource
142+
* @since 6.1.4
143+
*/
144+
public static RouterFunction<ServerResponse> resource(RequestPredicate predicate, Resource resource) {
145+
return resources(new PredicateResourceLookupFunction(predicate, resource), (consumerResource, httpHeaders) -> {});
146+
}
147+
148+
/**
149+
* Route requests that match the given predicate to the given resource.
150+
* For instance
151+
* <pre class="code">
152+
* Resource resource = new ClassPathResource("static/index.html")
153+
* RouterFunction&lt;ServerResponse&gt; resources = RouterFunctions.resource(path("/api/**").negate(), resource);
154+
* </pre>
155+
* @param predicate predicate to match
156+
* @param resource the resources to serve
157+
* @param headersConsumer provides access to the HTTP headers for served resources
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+
BiConsumer<Resource, HttpHeaders> headersConsumer) {
163+
return resources(new PredicateResourceLookupFunction(predicate, resource), headersConsumer);
164+
}
165+
131166
/**
132167
* Route requests that match the given pattern to resources relative to the given root location.
133168
* For instance
@@ -602,6 +637,36 @@ public interface Builder {
602637
*/
603638
Builder add(RouterFunction<ServerResponse> routerFunction);
604639

640+
/**
641+
* Route requests that match the given predicate to the given resource.
642+
* For instance
643+
* <pre class="code">
644+
* Resource resource = new ClassPathResource("static/index.html")
645+
* RouterFunction&lt;ServerResponse&gt; resources = RouterFunctions.resource(path("/api/**").negate(), resource);
646+
* </pre>
647+
* @param predicate predicate to match
648+
* @param resource the resources to serve
649+
* @return a router function that routes to a resource
650+
* @since 6.1.4
651+
*/
652+
Builder resource(RequestPredicate predicate, Resource resource);
653+
654+
/**
655+
* Route requests that match the given predicate to the given resource.
656+
* For instance
657+
* <pre class="code">
658+
* Resource resource = new ClassPathResource("static/index.html")
659+
* RouterFunction&lt;ServerResponse&gt; resources = RouterFunctions.resource(path("/api/**").negate(), resource);
660+
* </pre>
661+
* @param predicate predicate to match
662+
* @param resource the resources to serve
663+
* @param headersConsumer provides access to the HTTP headers for served resources
664+
* @return a router function that routes to a resource
665+
* @since 6.1.4
666+
*/
667+
Builder resource(RequestPredicate predicate, Resource resource, BiConsumer<Resource, HttpHeaders> headersConsumer);
668+
669+
605670
/**
606671
* Route requests that match the given pattern to resources relative to the given root location.
607672
* For instance

spring-webmvc/src/main/kotlin/org/springframework/web/servlet/function/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.
@@ -612,6 +612,15 @@ class RouterFunctionDsl internal constructor (private val init: (RouterFunctionD
612612
builder.add(RouterFunctions.route(RequestPredicates.path(this), HandlerFunction(f)))
613613
}
614614

615+
/**
616+
* Route requests that match the given predicate to the given resource.
617+
* @see RouterFunctions.resource
618+
* @since 6.1.4
619+
*/
620+
fun resource(predicate: RequestPredicate, resource: Resource) {
621+
builder.resource(predicate, resource)
622+
}
623+
615624
/**
616625
* Route requests that match the given pattern to resources relative to the given root location.
617626
* @see RouterFunctions.resources

spring-webmvc/src/test/java/org/springframework/web/servlet/function/RouterFunctionBuilderTests.java

+19
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,13 @@
3939
import static java.util.Collections.emptyList;
4040
import static org.assertj.core.api.Assertions.assertThat;
4141
import static org.springframework.web.servlet.function.RequestPredicates.HEAD;
42+
import static org.springframework.web.servlet.function.RequestPredicates.path;
4243

4344
/**
4445
* Tests for {@link RouterFunctionBuilder}.
4546
*
4647
* @author Arjen Poutsma
48+
* @author Sebastien Deleuze
4749
*/
4850
class RouterFunctionBuilderTests {
4951

@@ -96,6 +98,23 @@ private static ServerResponse handle(HandlerFunction<ServerResponse> handlerFunc
9698
}
9799
}
98100

101+
@Test
102+
void resource() {
103+
Resource resource = new ClassPathResource("/org/springframework/web/servlet/function/response.txt");
104+
assertThat(resource.exists()).isTrue();
105+
106+
RouterFunction<ServerResponse> route = RouterFunctions.route()
107+
.resource(path("/test"), resource)
108+
.build();
109+
110+
ServerRequest resourceRequest = initRequest("GET", "/test");
111+
112+
Optional<HttpStatusCode> responseStatus = route.route(resourceRequest)
113+
.map(handlerFunction -> handle(handlerFunction, resourceRequest))
114+
.map(ServerResponse::statusCode);
115+
assertThat(responseStatus).contains(HttpStatus.OK);
116+
}
117+
99118
@Test
100119
void resources() {
101120
Resource resource = new ClassPathResource("/org/springframework/web/servlet/function/");

spring-webmvc/src/test/kotlin/org/springframework/web/servlet/function/RouterFunctionDslTests.kt

+18-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 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.
@@ -74,14 +74,6 @@ class RouterFunctionDslTests {
7474
assertThat(sampleRouter().route(request).isPresent).isTrue()
7575
}
7676

77-
@Test
78-
fun resourceByPath() {
79-
val servletRequest = PathPatternsTestUtils.initRequest(
80-
"GET", "/org/springframework/web/servlet/function/response.txt", true)
81-
val request = DefaultServerRequest(servletRequest, emptyList())
82-
assertThat(sampleRouter().route(request).isPresent).isTrue()
83-
}
84-
8577
@Test
8678
fun method() {
8779
val servletRequest = PathPatternsTestUtils.initRequest("PATCH", "/", true)
@@ -98,6 +90,20 @@ class RouterFunctionDslTests {
9890

9991
@Test
10092
fun resource() {
93+
val servletRequest = PathPatternsTestUtils.initRequest("GET","/response2.txt", true)
94+
val request = DefaultServerRequest(servletRequest, emptyList())
95+
assertThat(sampleRouter().route(request).isPresent).isTrue()
96+
}
97+
98+
@Test
99+
fun resources() {
100+
val servletRequest = PathPatternsTestUtils.initRequest("GET", "/resources/response.txt", true)
101+
val request = DefaultServerRequest(servletRequest, emptyList())
102+
assertThat(sampleRouter().route(request).isPresent).isTrue()
103+
}
104+
105+
@Test
106+
fun resourcesLookupFunction() {
101107
val servletRequest = PathPatternsTestUtils.initRequest("GET", "/response.txt", true)
102108
val request = DefaultServerRequest(servletRequest, emptyList())
103109
assertThat(sampleRouter().route(request).isPresent).isTrue()
@@ -168,8 +174,9 @@ class RouterFunctionDslTests {
168174
GET("/api/foo/", ::handle)
169175
}
170176
headers({ it.header("bar").isNotEmpty() }, ::handle)
171-
resources("/org/springframework/web/servlet/function/**",
172-
ClassPathResource("/org/springframework/web/servlet/function/response.txt"))
177+
resource(path("/response2.txt"), ClassPathResource("/org/springframework/web/servlet/function/response.txt"))
178+
resources("/resources/**",
179+
ClassPathResource("/org/springframework/web/servlet/function/"))
173180
resources {
174181
if (it.path() == "/response.txt") {
175182
ClassPathResource("/org/springframework/web/servlet/function/response.txt")

0 commit comments

Comments
 (0)