From 4346d15cb5b68f88f5dd13a6ee3550d24db3ab33 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 5 Sep 2016 21:57:32 -0400 Subject: [PATCH] Resource handling configuration Issue: SPR-14521 --- .../config/ResourceChainRegistration.java | 132 ++++++++++ .../config/ResourceHandlerRegistration.java | 160 ++++++++++++ .../config/ResourceHandlerRegistry.java | 158 ++++++++++++ .../config/WebReactiveConfiguration.java | 50 +++- .../config/ResourceHandlerRegistryTests.java | 231 ++++++++++++++++++ .../web/reactive/config/testStylesheet.css | 1 + 6 files changed, 730 insertions(+), 2 deletions(-) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistry.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java create mode 100644 spring-web-reactive/src/test/resources/org/springframework/web/reactive/config/testStylesheet.css diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java new file mode 100644 index 0000000000..f51b04f186 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceChainRegistration.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.cache.Cache; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.web.reactive.resource.CachingResourceResolver; +import org.springframework.web.reactive.resource.CachingResourceTransformer; +import org.springframework.web.reactive.resource.CssLinkResourceTransformer; +import org.springframework.web.reactive.resource.PathResourceResolver; +import org.springframework.web.reactive.resource.ResourceResolver; +import org.springframework.web.reactive.resource.ResourceTransformer; +import org.springframework.web.reactive.resource.VersionResourceResolver; +import org.springframework.web.reactive.resource.WebJarsResourceResolver; + +/** + * Assists with the registration of resource resolvers and transformers. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ResourceChainRegistration { + + private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache"; + + private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent( + "org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader()); + + + private final List resolvers = new ArrayList<>(4); + + private final List transformers = new ArrayList<>(4); + + private boolean hasVersionResolver; + + private boolean hasPathResolver; + + private boolean hasCssLinkTransformer; + + private boolean hasWebjarsResolver; + + + public ResourceChainRegistration(boolean cacheResources) { + this(cacheResources, cacheResources ? new ConcurrentMapCache(DEFAULT_CACHE_NAME) : null); + } + + public ResourceChainRegistration(boolean cacheResources, Cache cache) { + Assert.isTrue(!cacheResources || cache != null, "'cache' is required when cacheResources=true"); + if (cacheResources) { + this.resolvers.add(new CachingResourceResolver(cache)); + this.transformers.add(new CachingResourceTransformer(cache)); + } + } + + + /** + * Add a resource resolver to the chain. + * @param resolver the resolver to add + * @return the current instance for chained method invocation + */ + public ResourceChainRegistration addResolver(ResourceResolver resolver) { + Assert.notNull(resolver, "The provided ResourceResolver should not be null"); + this.resolvers.add(resolver); + if (resolver instanceof VersionResourceResolver) { + this.hasVersionResolver = true; + } + else if (resolver instanceof PathResourceResolver) { + this.hasPathResolver = true; + } + else if (resolver instanceof WebJarsResourceResolver) { + this.hasWebjarsResolver = true; + } + return this; + } + + /** + * Add a resource transformer to the chain. + * @param transformer the transformer to add + * @return the current instance for chained method invocation + */ + public ResourceChainRegistration addTransformer(ResourceTransformer transformer) { + Assert.notNull(transformer, "The provided ResourceTransformer should not be null"); + this.transformers.add(transformer); + if (transformer instanceof CssLinkResourceTransformer) { + this.hasCssLinkTransformer = true; + } + return this; + } + + protected List getResourceResolvers() { + if (!this.hasPathResolver) { + List result = new ArrayList<>(this.resolvers); + if (isWebJarsAssetLocatorPresent && !this.hasWebjarsResolver) { + result.add(new WebJarsResourceResolver()); + } + result.add(new PathResourceResolver()); + return result; + } + return this.resolvers; + } + + protected List getResourceTransformers() { + if (this.hasVersionResolver && !this.hasCssLinkTransformer) { + List result = new ArrayList<>(this.transformers); + boolean hasTransformers = !this.transformers.isEmpty(); + boolean hasCaching = hasTransformers && this.transformers.get(0) instanceof CachingResourceTransformer; + result.add(hasCaching ? 1 : 0, new CssLinkResourceTransformer()); + return result; + } + return this.transformers; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java new file mode 100644 index 0000000000..6182b21dce --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistration.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.cache.Cache; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.http.CacheControl; +import org.springframework.util.Assert; +import org.springframework.web.reactive.resource.ResourceWebHandler; + +/** + * Assist with creating and configuring a static resources handler. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ResourceHandlerRegistration { + + private final ResourceLoader resourceLoader; + + private final String[] pathPatterns; + + private final List locations = new ArrayList<>(); + + private CacheControl cacheControl; + + private ResourceChainRegistration resourceChainRegistration; + + + /** + * Create a {@link ResourceHandlerRegistration} instance. + * @param resourceLoader a resource loader for turning a String location + * into a {@link Resource} + * @param pathPatterns one or more resource URL path patterns + */ + public ResourceHandlerRegistration(ResourceLoader resourceLoader, String... pathPatterns) { + Assert.notEmpty(pathPatterns, "At least one path pattern is required for resource handling."); + this.resourceLoader = resourceLoader; + this.pathPatterns = pathPatterns; + } + + + /** + * Add one or more resource locations from which to serve static content. + * Each location must point to a valid directory. Multiple locations may + * be specified as a comma-separated list, and the locations will be checked + * for a given resource in the order specified. + * + *

For example, {{@code "/"}, + * {@code "classpath:/META-INF/public-web-resources/"}} allows resources to + * be served both from the web application root and from any JAR on the + * classpath that contains a {@code /META-INF/public-web-resources/} directory, + * with resources in the web application root taking precedence. + * @return the same {@link ResourceHandlerRegistration} instance, for + * chained method invocation + */ + public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) { + for (String location : resourceLocations) { + this.locations.add(resourceLoader.getResource(location)); + } + return this; + } + + /** + * Specify the {@link CacheControl} which should be used + * by the resource handler. + * + * @param cacheControl the CacheControl configuration to use + * @return the same {@link ResourceHandlerRegistration} instance, for + * chained method invocation + */ + public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) { + this.cacheControl = cacheControl; + return this; + } + + /** + * Configure a chain of resource resolvers and transformers to use. This + * can be useful, for example, to apply a version strategy to resource URLs. + * + *

If this method is not invoked, by default only a simple + * {@code PathResourceResolver} is used in order to match URL paths to + * resources under the configured locations. + * + * @param cacheResources whether to cache the result of resource resolution; + * setting this to "true" is recommended for production (and "false" for + * development, especially when applying a version strategy) + * @return the same {@link ResourceHandlerRegistration} instance, for + * chained method invocation + */ + public ResourceChainRegistration resourceChain(boolean cacheResources) { + this.resourceChainRegistration = new ResourceChainRegistration(cacheResources); + return this.resourceChainRegistration; + } + + /** + * Configure a chain of resource resolvers and transformers to use. This + * can be useful, for example, to apply a version strategy to resource URLs. + * + *

If this method is not invoked, by default only a simple + * {@code PathResourceResolver} is used in order to match URL paths to + * resources under the configured locations. + * + * @param cacheResources whether to cache the result of resource resolution; + * setting this to "true" is recommended for production (and "false" for + * development, especially when applying a version strategy + * @param cache the cache to use for storing resolved and transformed resources; + * by default a {@link org.springframework.cache.concurrent.ConcurrentMapCache} + * is used. Since Resources aren't serializable and can be dependent on the + * application host, one should not use a distributed cache but rather an + * in-memory cache. + * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation + */ + public ResourceChainRegistration resourceChain(boolean cacheResources, Cache cache) { + this.resourceChainRegistration = new ResourceChainRegistration(cacheResources, cache); + return this.resourceChainRegistration; + } + + /** + * Returns the URL path patterns for the resource handler. + */ + protected String[] getPathPatterns() { + return this.pathPatterns; + } + + /** + * Returns a {@link ResourceWebHandler} instance. + */ + protected ResourceWebHandler getRequestHandler() { + ResourceWebHandler handler = new ResourceWebHandler(); + if (this.resourceChainRegistration != null) { + handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers()); + handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers()); + } + handler.setLocations(this.locations); + if (this.cacheControl != null) { + handler.setCacheControl(this.cacheControl); + } + return handler; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistry.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistry.java new file mode 100644 index 0000000000..803f29463a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/ResourceHandlerRegistry.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.context.ApplicationContext; +import org.springframework.util.Assert; +import org.springframework.web.reactive.accept.CompositeContentTypeResolver; +import org.springframework.web.reactive.handler.AbstractHandlerMapping; +import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; +import org.springframework.web.reactive.resource.ResourceWebHandler; +import org.springframework.web.server.WebHandler; + +/** + * Stores registrations of resource handlers for serving static resources such + * as images, css files and others through Spring MVC including setting cache + * headers optimized for efficient loading in a web browser. Resources can be + * served out of locations under web application root, from the classpath, and + * others. + * + *

To create a resource handler, use {@link #addResourceHandler(String...)} + * providing the URL path patterns for which the handler should be invoked to + * serve static resources (e.g. {@code "/resources/**"}). + * + *

Then use additional methods on the returned + * {@link ResourceHandlerRegistration} to add one or more locations from which + * to serve static content from (e.g. {{@code "/"}, + * {@code "classpath:/META-INF/public-web-resources/"}}) or to specify a cache + * period for served resources. + * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ResourceHandlerRegistry { + + private final ApplicationContext applicationContext; + + private final CompositeContentTypeResolver contentTypeResolver; + + private final List registrations = new ArrayList<>(); + + private int order = Integer.MAX_VALUE -1; + + + /** + * Create a new resource handler registry for the given application context. + * @param applicationContext the Spring application context + */ + public ResourceHandlerRegistry(ApplicationContext applicationContext) { + this(applicationContext, null); + } + + /** + * Create a new resource handler registry for the given application context. + * @param applicationContext the Spring application context + * @param contentTypeResolver the content type resolver to use + */ + public ResourceHandlerRegistry(ApplicationContext applicationContext, + CompositeContentTypeResolver contentTypeResolver) { + + Assert.notNull(applicationContext, "ApplicationContext is required"); + this.applicationContext = applicationContext; + this.contentTypeResolver = contentTypeResolver; + } + + + /** + * Add a resource handler for serving static resources based on the specified + * URL path patterns. The handler will be invoked for every incoming request + * that matches to one of the specified path patterns. + * + *

Patterns like {@code "/static/**"} or {@code "/css/{filename:\\w+\\.css}"} + * are allowed. See {@link org.springframework.util.AntPathMatcher} for more + * details on the syntax. + * @return A {@link ResourceHandlerRegistration} to use to further + * configure the registered resource handler + */ + public ResourceHandlerRegistration addResourceHandler(String... patterns) { + ResourceHandlerRegistration registration = + new ResourceHandlerRegistration(this.applicationContext, patterns); + this.registrations.add(registration); + return registration; + } + + /** + * Whether a resource handler has already been registered for the given path pattern. + */ + public boolean hasMappingForPattern(String pathPattern) { + for (ResourceHandlerRegistration registration : this.registrations) { + if (Arrays.asList(registration.getPathPatterns()).contains(pathPattern)) { + return true; + } + } + return false; + } + + /** + * Specify the order to use for resource handling relative to other + * {@code HandlerMapping}s configured in the Spring configuration. + *

The default value used is {@code Integer.MAX_VALUE-1}. + */ + public ResourceHandlerRegistry setOrder(int order) { + this.order = order; + return this; + } + + /** + * Return a handler mapping with the mapped resource handlers; or {@code null} in case + * of no registrations. + */ + protected AbstractHandlerMapping getHandlerMapping() { + if (this.registrations.isEmpty()) { + return null; + } + + Map urlMap = new LinkedHashMap<>(); + for (ResourceHandlerRegistration registration : this.registrations) { + for (String pathPattern : registration.getPathPatterns()) { + ResourceWebHandler handler = registration.getRequestHandler(); + handler.setContentTypeResolver(this.contentTypeResolver); + try { + handler.afterPropertiesSet(); + handler.afterSingletonsInstantiated(); + } + catch (Exception ex) { + throw new BeanInitializationException("Failed to init ResourceHttpRequestHandler", ex); + } + urlMap.put(pathPattern, handler); + } + } + + SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping(); + handlerMapping.setOrder(order); + handlerMapping.setUrlMap(urlMap); + return handlerMapping; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java index 96941fcf48..2d2314010a 100644 --- a/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java @@ -21,6 +21,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import reactor.core.publisher.Mono; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.context.ApplicationContext; @@ -52,8 +54,10 @@ import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.util.ClassUtils; import org.springframework.validation.Errors; import org.springframework.validation.Validator; -import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.accept.CompositeContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder; +import org.springframework.web.reactive.handler.AbstractHandlerMapping; import org.springframework.web.reactive.result.SimpleHandlerAdapter; import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter; @@ -62,6 +66,7 @@ import org.springframework.web.reactive.result.method.annotation.ResponseBodyRes import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler; import org.springframework.web.reactive.result.view.ViewResolutionResultHandler; import org.springframework.web.reactive.result.view.ViewResolver; +import org.springframework.web.server.ServerWebExchange; /** * The main class for Spring Web Reactive configuration. @@ -135,7 +140,7 @@ public class WebReactiveConfiguration implements ApplicationContextAware { } @Bean - public RequestedContentTypeResolver mvcContentTypeResolver() { + public CompositeContentTypeResolver mvcContentTypeResolver() { RequestedContentTypeResolverBuilder builder = new RequestedContentTypeResolverBuilder(); builder.mediaTypes(getDefaultMediaTypeMappings()); configureRequestedContentTypeResolver(builder); @@ -178,6 +183,39 @@ public class WebReactiveConfiguration implements ApplicationContextAware { public void configurePathMatching(PathMatchConfigurer configurer) { } + /** + * Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped + * resource handlers. To configure resource handling, override + * {@link #addResourceHandlers}. + */ + @Bean + public HandlerMapping resourceHandlerMapping() { + ResourceHandlerRegistry registry = + new ResourceHandlerRegistry(this.applicationContext, mvcContentTypeResolver()); + addResourceHandlers(registry); + + AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); + if (handlerMapping != null) { + if (getPathMatchConfigurer() != null) { + handlerMapping.setPathMatcher(getPathMatchConfigurer().getPathMatcher()); + } + if (getPathMatchConfigurer() != null) { + handlerMapping.setPathHelper(getPathMatchConfigurer().getPathHelper()); + } + } + else { + handlerMapping = new EmptyHandlerMapping(); + } + return handlerMapping; + } + + /** + * Override this method to add resource handlers for serving static resources. + * @see ResourceHandlerRegistry + */ + protected void addResourceHandlers(ResourceHandlerRegistry registry) { + } + @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); @@ -397,6 +435,14 @@ public class WebReactiveConfiguration implements ApplicationContextAware { } + private static final class EmptyHandlerMapping extends AbstractHandlerMapping { + + @Override + public Mono getHandler(ServerWebExchange exchange) { + return Mono.empty(); + } + } + private static final class NoOpValidator implements Validator { @Override diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java new file mode 100644 index 0000000000..fed09be2e7 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/config/ResourceHandlerRegistryTests.java @@ -0,0 +1,231 @@ +/* + * Copyright 2002-2016 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 + * + * http://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.config; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.buffer.support.DataBufferTestUtils; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpMethod; +import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; +import org.springframework.tests.TestSubscriber; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; +import org.springframework.web.reactive.resource.AppCacheManifestTransformer; +import org.springframework.web.reactive.resource.CachingResourceResolver; +import org.springframework.web.reactive.resource.CachingResourceTransformer; +import org.springframework.web.reactive.resource.CssLinkResourceTransformer; +import org.springframework.web.reactive.resource.PathResourceResolver; +import org.springframework.web.reactive.resource.ResourceResolver; +import org.springframework.web.reactive.resource.ResourceTransformer; +import org.springframework.web.reactive.resource.ResourceWebHandler; +import org.springframework.web.reactive.resource.VersionResourceResolver; +import org.springframework.web.reactive.resource.WebJarsResourceResolver; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.DefaultWebSessionManager; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for {@link ResourceHandlerRegistry}. + * + * @author Rossen Stoyanchev + */ +public class ResourceHandlerRegistryTests { + + private ResourceHandlerRegistry registry; + + private ResourceHandlerRegistration registration; + + private ServerWebExchange exchange; + + private MockServerHttpResponse response; + + + @Before + public void setUp() { + this.registry = new ResourceHandlerRegistry(new GenericApplicationContext()); + this.registration = this.registry.addResourceHandler("/resources/**"); + this.registration.addResourceLocations("classpath:org/springframework/web/reactive/config/"); + + MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, ""); + this.response = new MockServerHttpResponse(); + WebSessionManager manager = new DefaultWebSessionManager(); + this.exchange = new DefaultServerWebExchange(request, this.response, manager); + } + + + @Test + public void noResourceHandlers() throws Exception { + this.registry = new ResourceHandlerRegistry(new GenericApplicationContext()); + assertNull(this.registry.getHandlerMapping()); + } + + @Test + public void mapPathToLocation() throws Exception { + this.exchange.getAttributes().put( + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/testStylesheet.css"); + + ResourceWebHandler handler = getHandler("/resources/**"); + handler.handle(this.exchange).blockMillis(5000); + + TestSubscriber.subscribe(this.response.getBody()) + .assertValuesWith(buf -> assertEquals("test stylesheet content", + DataBufferTestUtils.dumpString(buf, StandardCharsets.UTF_8))); + } + + @Test + public void cacheControl() { + assertThat(getHandler("/resources/**").getCacheControl(), + Matchers.nullValue()); + + this.registration.setCacheControl(CacheControl.noCache().cachePrivate()); + assertThat(getHandler("/resources/**").getCacheControl().getHeaderValue(), + Matchers.equalTo(CacheControl.noCache().cachePrivate().getHeaderValue())); + } + + @Test + public void order() { + assertEquals(Integer.MAX_VALUE -1, this.registry.getHandlerMapping().getOrder()); + + this.registry.setOrder(0); + assertEquals(0, this.registry.getHandlerMapping().getOrder()); + } + + @Test + public void hasMappingForPattern() { + assertTrue(this.registry.hasMappingForPattern("/resources/**")); + assertFalse(this.registry.hasMappingForPattern("/whatever")); + } + + @Test + public void resourceChain() throws Exception { + ResourceResolver mockResolver = Mockito.mock(ResourceResolver.class); + ResourceTransformer mockTransformer = Mockito.mock(ResourceTransformer.class); + this.registration.resourceChain(true).addResolver(mockResolver).addTransformer(mockTransformer); + + ResourceWebHandler handler = getHandler("/resources/**"); + List resolvers = handler.getResourceResolvers(); + assertThat(resolvers.toString(), resolvers, Matchers.hasSize(4)); + assertThat(resolvers.get(0), Matchers.instanceOf(CachingResourceResolver.class)); + CachingResourceResolver cachingResolver = (CachingResourceResolver) resolvers.get(0); + assertThat(cachingResolver.getCache(), Matchers.instanceOf(ConcurrentMapCache.class)); + assertThat(resolvers.get(1), Matchers.equalTo(mockResolver)); + assertThat(resolvers.get(2), Matchers.instanceOf(WebJarsResourceResolver.class)); + assertThat(resolvers.get(3), Matchers.instanceOf(PathResourceResolver.class)); + + List transformers = handler.getResourceTransformers(); + assertThat(transformers, Matchers.hasSize(2)); + assertThat(transformers.get(0), Matchers.instanceOf(CachingResourceTransformer.class)); + assertThat(transformers.get(1), Matchers.equalTo(mockTransformer)); + } + + @Test + public void resourceChainWithoutCaching() throws Exception { + this.registration.resourceChain(false); + + ResourceWebHandler handler = getHandler("/resources/**"); + List resolvers = handler.getResourceResolvers(); + assertThat(resolvers, Matchers.hasSize(2)); + assertThat(resolvers.get(0), Matchers.instanceOf(WebJarsResourceResolver.class)); + assertThat(resolvers.get(1), Matchers.instanceOf(PathResourceResolver.class)); + + List transformers = handler.getResourceTransformers(); + assertThat(transformers, Matchers.hasSize(0)); + } + + @Test + public void resourceChainWithVersionResolver() throws Exception { + VersionResourceResolver versionResolver = new VersionResourceResolver() + .addFixedVersionStrategy("fixed", "/**/*.js") + .addContentVersionStrategy("/**"); + + this.registration.resourceChain(true).addResolver(versionResolver) + .addTransformer(new AppCacheManifestTransformer()); + + ResourceWebHandler handler = getHandler("/resources/**"); + List resolvers = handler.getResourceResolvers(); + assertThat(resolvers.toString(), resolvers, Matchers.hasSize(4)); + assertThat(resolvers.get(0), Matchers.instanceOf(CachingResourceResolver.class)); + assertThat(resolvers.get(1), Matchers.sameInstance(versionResolver)); + assertThat(resolvers.get(2), Matchers.instanceOf(WebJarsResourceResolver.class)); + assertThat(resolvers.get(3), Matchers.instanceOf(PathResourceResolver.class)); + + List transformers = handler.getResourceTransformers(); + assertThat(transformers, Matchers.hasSize(3)); + assertThat(transformers.get(0), Matchers.instanceOf(CachingResourceTransformer.class)); + assertThat(transformers.get(1), Matchers.instanceOf(CssLinkResourceTransformer.class)); + assertThat(transformers.get(2), Matchers.instanceOf(AppCacheManifestTransformer.class)); + } + + @Test + public void resourceChainWithOverrides() throws Exception { + CachingResourceResolver cachingResolver = Mockito.mock(CachingResourceResolver.class); + VersionResourceResolver versionResolver = Mockito.mock(VersionResourceResolver.class); + WebJarsResourceResolver webjarsResolver = Mockito.mock(WebJarsResourceResolver.class); + PathResourceResolver pathResourceResolver = new PathResourceResolver(); + CachingResourceTransformer cachingTransformer = Mockito.mock(CachingResourceTransformer.class); + AppCacheManifestTransformer appCacheTransformer = Mockito.mock(AppCacheManifestTransformer.class); + CssLinkResourceTransformer cssLinkTransformer = new CssLinkResourceTransformer(); + + this.registration.setCacheControl(CacheControl.maxAge(3600, TimeUnit.MILLISECONDS)) + .resourceChain(false) + .addResolver(cachingResolver) + .addResolver(versionResolver) + .addResolver(webjarsResolver) + .addResolver(pathResourceResolver) + .addTransformer(cachingTransformer) + .addTransformer(appCacheTransformer) + .addTransformer(cssLinkTransformer); + + ResourceWebHandler handler = getHandler("/resources/**"); + List resolvers = handler.getResourceResolvers(); + assertThat(resolvers.toString(), resolvers, Matchers.hasSize(4)); + assertThat(resolvers.get(0), Matchers.sameInstance(cachingResolver)); + assertThat(resolvers.get(1), Matchers.sameInstance(versionResolver)); + assertThat(resolvers.get(2), Matchers.sameInstance(webjarsResolver)); + assertThat(resolvers.get(3), Matchers.sameInstance(pathResourceResolver)); + + List transformers = handler.getResourceTransformers(); + assertThat(transformers, Matchers.hasSize(3)); + assertThat(transformers.get(0), Matchers.sameInstance(cachingTransformer)); + assertThat(transformers.get(1), Matchers.sameInstance(appCacheTransformer)); + assertThat(transformers.get(2), Matchers.sameInstance(cssLinkTransformer)); + } + + private ResourceWebHandler getHandler(String pathPattern) { + SimpleUrlHandlerMapping mapping = (SimpleUrlHandlerMapping) this.registry.getHandlerMapping(); + return (ResourceWebHandler) mapping.getUrlMap().get(pathPattern); + } + +} diff --git a/spring-web-reactive/src/test/resources/org/springframework/web/reactive/config/testStylesheet.css b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/config/testStylesheet.css new file mode 100644 index 0000000000..e15d46f06b --- /dev/null +++ b/spring-web-reactive/src/test/resources/org/springframework/web/reactive/config/testStylesheet.css @@ -0,0 +1 @@ +test stylesheet content \ No newline at end of file