From ad50de169c0485ce7c0fb5cb8aa5ac94b68cbcf6 Mon Sep 17 00:00:00 2001 From: Jakob Fels Date: Mon, 16 Jan 2023 21:00:53 +0100 Subject: [PATCH] Provide access to HTTP headers in resource routing This commit adds additional overloaded methods that allow for HTTP header manipulation of served resources. Closes gh-29985 --- .../DefaultResourceCacheLookupStrategy.java | 33 +++++++++++++++++ .../server/ResourceCacheLookupStrategy.java | 36 +++++++++++++++++++ .../server/ResourceHandlerFunction.java | 17 +++++++-- .../server/RouterFunctionBuilder.java | 10 ++++++ .../function/server/RouterFunctions.java | 22 ++++++++++-- .../server/RouterFunctionBuilderTests.java | 25 +++++++++++++ 6 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultResourceCacheLookupStrategy.java create mode 100644 spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceCacheLookupStrategy.java diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultResourceCacheLookupStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultResourceCacheLookupStrategy.java new file mode 100644 index 0000000000..2cc05e120a --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultResourceCacheLookupStrategy.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2023 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 org.springframework.core.io.Resource; +import org.springframework.http.CacheControl; + +/** + * Default lookup that performs no caching. + * @author Jakob Fels + */ +public class DefaultResourceCacheLookupStrategy implements ResourceCacheLookupStrategy { + + @Override + public CacheControl lookupCacheControl(Resource resource) { + return CacheControl.empty(); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceCacheLookupStrategy.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceCacheLookupStrategy.java new file mode 100644 index 0000000000..462cc19280 --- /dev/null +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceCacheLookupStrategy.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2023 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 org.springframework.core.io.Resource; +import org.springframework.http.CacheControl; + +/** + * Strategy interface to allow for looking up cache control for a given resource. + * + * @author Jakob Fels + */ +public interface ResourceCacheLookupStrategy { + + + static ResourceCacheLookupStrategy noCaching() { + return new DefaultResourceCacheLookupStrategy(); + } + + CacheControl lookupCacheControl(Resource resource); + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java index 66f8c097cf..783ce93e95 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/ResourceHandlerFunction.java @@ -27,6 +27,7 @@ import java.util.Set; import reactor.core.publisher.Mono; import org.springframework.core.io.Resource; +import org.springframework.http.CacheControl; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.lang.Nullable; @@ -45,10 +46,18 @@ class ResourceHandlerFunction implements HandlerFunction { private final Resource resource; + private final CacheControl cacheControl; public ResourceHandlerFunction(Resource resource) { this.resource = resource; + this.cacheControl = CacheControl.empty(); + } + + + public ResourceHandlerFunction(Resource resource, ResourceCacheLookupStrategy strategy) { + this.resource = resource; + this.cacheControl = strategy.lookupCacheControl(resource); } @@ -56,12 +65,16 @@ class ResourceHandlerFunction implements HandlerFunction { public Mono handle(ServerRequest request) { HttpMethod method = request.method(); if (HttpMethod.GET.equals(method)) { - return EntityResponse.fromObject(this.resource).build() + return EntityResponse.fromObject(this.resource) + .cacheControl(this.cacheControl) + .build() .map(response -> response); } else if (HttpMethod.HEAD.equals(method)) { Resource headResource = new HeadMethodResource(this.resource); - return EntityResponse.fromObject(headResource).build() + return EntityResponse.fromObject(headResource) + .cacheControl(this.cacheControl) + .build() .map(response -> response); } else if (HttpMethod.OPTIONS.equals(method)) { 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 f54ae924a0..308d317140 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 @@ -241,11 +241,21 @@ class RouterFunctionBuilder implements RouterFunctions.Builder { return add(RouterFunctions.resources(pattern, location)); } + @Override + public RouterFunctions.Builder resources(String pattern, Resource location, ResourceCacheLookupStrategy resourceCacheLookupStrategy) { + return add(RouterFunctions.resources(pattern,location,resourceCacheLookupStrategy)); + } + @Override public RouterFunctions.Builder resources(Function> lookupFunction) { return add(RouterFunctions.resources(lookupFunction)); } + @Override + public RouterFunctions.Builder resources(Function> lookupFunction, ResourceCacheLookupStrategy resourceCacheLookupStrategy) { + return add(RouterFunctions.resources(lookupFunction,resourceCacheLookupStrategy)); + } + @Override public RouterFunctions.Builder nest(RequestPredicate predicate, Consumer builderConsumer) { 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 f55838488e..9de4c49297 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 @@ -156,7 +156,10 @@ public abstract class RouterFunctions { * @see #resourceLookupFunction(String, Resource) */ public static RouterFunction resources(String pattern, Resource location) { - return resources(resourceLookupFunction(pattern, location)); + return resources(resourceLookupFunction(pattern, location), ResourceCacheLookupStrategy.noCaching()); + } + public static RouterFunction resources(String pattern, Resource location, ResourceCacheLookupStrategy lookupStrategy) { + return resources(resourceLookupFunction(pattern, location), lookupStrategy); } /** @@ -186,7 +189,10 @@ public abstract class RouterFunctions { * @return a router function that routes to resources */ public static RouterFunction resources(Function> lookupFunction) { - return new ResourcesRouterFunction(lookupFunction); + return new ResourcesRouterFunction(lookupFunction, ResourceCacheLookupStrategy.noCaching()); + } + public static RouterFunction resources(Function> lookupFunction, ResourceCacheLookupStrategy lookupStrategy) { + return new ResourcesRouterFunction(lookupFunction, lookupStrategy); } /** @@ -652,6 +658,7 @@ public abstract class RouterFunctions { * @return this builder */ Builder resources(String pattern, Resource location); + Builder resources(String pattern, Resource location, ResourceCacheLookupStrategy resourceCacheLookupStrategy); /** * Route to resources using the provided lookup function. If the lookup function provides a @@ -661,6 +668,7 @@ public abstract class RouterFunctions { * @return this builder */ Builder resources(Function> lookupFunction); + Builder resources(Function> lookupFunction, ResourceCacheLookupStrategy resourceCacheLookupStrategy); /** * Route to the supplied router function if the given request predicate applies. This method @@ -1142,14 +1150,22 @@ public abstract class RouterFunctions { private final Function> lookupFunction; + private final ResourceCacheLookupStrategy lookupStrategy; + public ResourcesRouterFunction(Function> lookupFunction) { + this(lookupFunction, ResourceCacheLookupStrategy.noCaching()); + } + + public ResourcesRouterFunction(Function> lookupFunction, ResourceCacheLookupStrategy lookupStrategy) { Assert.notNull(lookupFunction, "Function must not be null"); + Assert.notNull(lookupStrategy, "Strategy must not be null"); this.lookupFunction = lookupFunction; + this.lookupStrategy = lookupStrategy; } @Override public Mono> route(ServerRequest request) { - return this.lookupFunction.apply(request).map(ResourceHandlerFunction::new); + return this.lookupFunction.apply(request).map(resource -> new ResourceHandlerFunction(resource, this.lookupStrategy)); } @Override 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 b91d2840ad..95e312ebf0 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 @@ -17,6 +17,7 @@ package org.springframework.web.reactive.function.server; import java.io.IOException; +import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Map; @@ -28,6 +29,8 @@ import reactor.test.StepVerifier; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; @@ -130,6 +133,28 @@ public class RouterFunctionBuilderTests { .verifyComplete(); } + @Test + public void resourcesCaching() { + Resource resource = new ClassPathResource("/org/springframework/web/reactive/function/server/"); + assertThat(resource.exists()).isTrue(); + + RouterFunction route = RouterFunctions.route() + .resources("/resources/**", resource, resource1 -> CacheControl.maxAge(Duration.ofSeconds(60))) + .build(); + + MockServerHttpRequest mockRequest = MockServerHttpRequest.get("https://localhost/resources/response.txt").build(); + ServerRequest resourceRequest = new DefaultServerRequest(MockServerWebExchange.from(mockRequest), Collections.emptyList()); + + Mono responseMono = route.route(resourceRequest) + .flatMap(handlerFunction -> handlerFunction.handle(resourceRequest)) + .map(ServerResponse::headers) + .mapNotNull(HttpHeaders::getCacheControl); + + StepVerifier.create(responseMono) + .expectNext("max-age=60") + .verifyComplete(); + } + @Test public void nest() { RouterFunction route = RouterFunctions.route()