Introduce support for webjars-locator-lite

This commit introduces support for org.webjars:webjars-locator-lite
via a new LiteWebJarsResourceResolver in Spring MVC and WebFlux, and
deprecates WebJarsResourceResolver which is performing a classpath
scanning that slows down application startup.

Closes gh-27619
This commit is contained in:
Sébastien Deleuze 2024-03-21 16:52:50 +01:00
parent 81bc586e15
commit 67edcde0a2
16 changed files with 565 additions and 15 deletions

View File

@ -143,6 +143,7 @@ dependencies {
api("org.testng:testng:7.9.0")
api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-core:0.55")
api("org.webjars:webjars-locator-lite:0.0.2")
api("org.xmlunit:xmlunit-assertj:2.9.1")
api("org.xmlunit:xmlunit-matchers:2.9.1")
api("org.yaml:snakeyaml:2.2")

View File

@ -32,6 +32,7 @@ dependencies {
optional("org.jetbrains.kotlin:kotlin-stdlib")
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
optional("org.webjars:webjars-locator-core")
optional("org.webjars:webjars-locator-lite")
testImplementation(testFixtures(project(":spring-beans")))
testImplementation(testFixtures(project(":spring-core")))
testImplementation(testFixtures(project(":spring-web")))

View File

@ -27,6 +27,7 @@ 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.LiteWebJarsResourceResolver;
import org.springframework.web.reactive.resource.PathResourceResolver;
import org.springframework.web.reactive.resource.ResourceResolver;
import org.springframework.web.reactive.resource.ResourceTransformer;
@ -43,9 +44,12 @@ public class ResourceChainRegistration {
private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache";
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
private static final boolean isWebJarAssetLocatorPresent = ClassUtils.isPresent(
"org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader());
private static final boolean isWebJarVersionLocatorPresent = ClassUtils.isPresent(
"org.webjars.WebJarVersionLocator", ResourceChainRegistration.class.getClassLoader());
private final List<ResourceResolver> resolvers = new ArrayList<>(4);
@ -79,6 +83,7 @@ public class ResourceChainRegistration {
* @param resolver the resolver to add
* @return the current instance for chained method invocation
*/
@SuppressWarnings("removal")
public ResourceChainRegistration addResolver(ResourceResolver resolver) {
Assert.notNull(resolver, "The provided ResourceResolver should not be null");
this.resolvers.add(resolver);
@ -88,7 +93,7 @@ public class ResourceChainRegistration {
else if (resolver instanceof PathResourceResolver) {
this.hasPathResolver = true;
}
else if (resolver instanceof WebJarsResourceResolver) {
else if (resolver instanceof WebJarsResourceResolver || resolver instanceof LiteWebJarsResourceResolver) {
this.hasWebjarsResolver = true;
}
return this;
@ -108,10 +113,14 @@ public class ResourceChainRegistration {
return this;
}
@SuppressWarnings("removal")
protected List<ResourceResolver> getResourceResolvers() {
if (!this.hasPathResolver) {
List<ResourceResolver> result = new ArrayList<>(this.resolvers);
if (isWebJarsAssetLocatorPresent && !this.hasWebjarsResolver) {
if (isWebJarVersionLocatorPresent && !this.hasWebjarsResolver) {
result.add(new LiteWebJarsResourceResolver());
}
else if (isWebJarAssetLocatorPresent && !this.hasWebjarsResolver) {
result.add(new WebJarsResourceResolver());
}
result.add(new PathResourceResolver());

View File

@ -0,0 +1,114 @@
/*
* Copyright 2002-2022 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.resource;
import java.util.List;
import org.webjars.WebJarVersionLocator;
import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerWebExchange;
/**
* A {@code ResourceResolver} that delegates to the chain to locate a resource and then
* attempts to find a matching versioned resource contained in a WebJar JAR file.
*
* <p>This allows WebJars.org users to write version agnostic paths in their templates,
* like {@code <script src="/webjars/jquery/jquery.min.js"/>}.
* This path will be resolved to the unique version {@code <script src="/webjars/jquery/1.2.0/jquery.min.js"/>},
* which is a better fit for HTTP caching and version management in applications.
*
* <p>This also resolves resources for version agnostic HTTP requests {@code "GET /jquery/jquery.min.js"}.
*
* <p>This resolver requires the {@code org.webjars:webjars-locator-lite} library
* on the classpath and is automatically registered if that library is present.
*
* @author Sebastien Deleuze
* @since 6.2
* @see <a href="https://www.webjars.org">webjars.org</a>
*/
public class LiteWebJarsResourceResolver extends AbstractResourceResolver {
private static final int WEBJARS_LOCATION_LENGTH = WebJarVersionLocator.WEBJARS_PATH_PREFIX.length() + 1;
private final WebJarVersionLocator webJarAssetLocator;
/**
* Create a {@code LiteWebJarsResourceResolver} with a default {@code WebJarVersionLocator} instance.
*/
public LiteWebJarsResourceResolver() {
this.webJarAssetLocator = new WebJarVersionLocator();
}
/**
* Create a {@code LiteWebJarsResourceResolver} with a custom {@code WebJarVersionLocator} instance,
* e.g. with a custom cache implementation.
*/
public LiteWebJarsResourceResolver(WebJarVersionLocator webJarAssetLocator) {
this.webJarAssetLocator = webJarAssetLocator;
}
@Override
protected Mono<Resource> resolveResourceInternal(@Nullable ServerWebExchange exchange,
String requestPath, List<? extends Resource> locations, ResourceResolverChain chain) {
return chain.resolveResource(exchange, requestPath, locations)
.switchIfEmpty(Mono.defer(() -> {
String webJarsResourcePath = findWebJarResourcePath(requestPath);
if (webJarsResourcePath != null) {
return chain.resolveResource(exchange, webJarsResourcePath, locations);
}
else {
return Mono.empty();
}
}));
}
@Override
protected Mono<String> resolveUrlPathInternal(String resourceUrlPath,
List<? extends Resource> locations, ResourceResolverChain chain) {
return chain.resolveUrlPath(resourceUrlPath, locations)
.switchIfEmpty(Mono.defer(() -> {
String webJarResourcePath = findWebJarResourcePath(resourceUrlPath);
if (webJarResourcePath != null) {
return chain.resolveUrlPath(webJarResourcePath, locations);
}
else {
return Mono.empty();
}
}));
}
@Nullable
protected String findWebJarResourcePath(String path) {
int startOffset = (path.startsWith("/") ? 1 : 0);
int endOffset = path.indexOf('/', 1);
if (endOffset != -1) {
String webjar = path.substring(startOffset, endOffset);
String partialPath = path.substring(endOffset + 1);
String webJarPath = this.webJarAssetLocator.fullPath(webjar, partialPath);
if (webJarPath != null) {
return webJarPath.substring(WEBJARS_LOCATION_LENGTH);
}
}
return null;
}
}

View File

@ -46,7 +46,9 @@ import org.springframework.web.server.ServerWebExchange;
* @author Brian Clozel
* @since 5.0
* @see <a href="https://www.webjars.org">webjars.org</a>
* @deprecated as of Spring Framework 6.2 in favor of {@link LiteWebJarsResourceResolver}
*/
@Deprecated(forRemoval = true)
public class WebJarsResourceResolver extends AbstractResourceResolver {
private static final String WEBJARS_LOCATION = "META-INF/resources/webjars/";

View File

@ -36,6 +36,7 @@ import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
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.LiteWebJarsResourceResolver;
import org.springframework.web.reactive.resource.PathResourceResolver;
import org.springframework.web.reactive.resource.ResourceResolver;
import org.springframework.web.reactive.resource.ResourceTransformer;
@ -142,7 +143,7 @@ class ResourceHandlerRegistryTests {
zero -> assertThat(zero).isInstanceOfSatisfying(CachingResourceResolver.class,
cachingResolver -> assertThat(cachingResolver.getCache()).isInstanceOf(ConcurrentMapCache.class)),
one -> assertThat(one).isEqualTo(mockResolver),
two -> assertThat(two).isInstanceOf(WebJarsResourceResolver.class),
two -> assertThat(two).isInstanceOf(LiteWebJarsResourceResolver.class),
three -> assertThat(three).isInstanceOf(PathResourceResolver.class));
assertThat(handler.getResourceTransformers()).satisfiesExactly(
zero -> assertThat(zero).isInstanceOf(CachingResourceTransformer.class),
@ -156,7 +157,7 @@ class ResourceHandlerRegistryTests {
ResourceWebHandler handler = getHandler("/resources/**");
assertThat(handler.getResourceResolvers()).hasExactlyElementsOfTypes(
WebJarsResourceResolver.class, PathResourceResolver.class);
LiteWebJarsResourceResolver.class, PathResourceResolver.class);
assertThat(handler.getResourceTransformers()).isEmpty();
}
@ -172,7 +173,7 @@ class ResourceHandlerRegistryTests {
assertThat(handler.getResourceResolvers()).satisfiesExactly(
zero -> assertThat(zero).isInstanceOf(CachingResourceResolver.class),
one -> assertThat(one).isSameAs(versionResolver),
two -> assertThat(two).isInstanceOf(WebJarsResourceResolver.class),
two -> assertThat(two).isInstanceOf(LiteWebJarsResourceResolver.class),
three -> assertThat(three).isInstanceOf(PathResourceResolver.class));
assertThat(handler.getResourceTransformers()).hasExactlyElementsOfTypes(
CachingResourceTransformer.class, CssLinkResourceTransformer.class);

View File

@ -0,0 +1,153 @@
/*
* 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.resource;
import java.time.Duration;
import java.util.List;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
import org.springframework.web.testfixture.server.MockServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link WebJarsResourceResolver}.
*
* @author Sebastien Deleuze
*/
class LiteWebJarsResourceResolverTests {
private static final Duration TIMEOUT = Duration.ofSeconds(1);
private List<Resource> locations = List.of(new ClassPathResource("/META-INF/resources/webjars"));
// for this to work, an actual WebJar must be on the test classpath
private LiteWebJarsResourceResolver resolver = new LiteWebJarsResourceResolver();
private ResourceResolverChain chain = mock();
private ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(""));
@Test
void resolveUrlExisting() {
String file = "/foo/2.3/foo.txt";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.just(file));
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT);
assertThat(actual).isEqualTo(file);
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
}
@Test
void resolveUrlExistingNotInJarFile() {
String file = "foo/foo.txt";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty());
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT);
assertThat(actual).isNull();
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
verify(this.chain, never()).resolveUrlPath("foo/2.3/foo.txt", this.locations);
}
@Test
void resolveUrlWebJarResource() {
String file = "underscorejs/underscore.js";
String expected = "underscorejs/1.8.3/underscore.js";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty());
given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(Mono.just(expected));
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT);
assertThat(actual).isEqualTo(expected);
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
}
@Test
void resolveUrlWebJarResourceNotFound() {
String file = "something/something.js";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(Mono.empty());
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain).block(TIMEOUT);
assertThat(actual).isNull();
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
verify(this.chain, never()).resolveUrlPath(null, this.locations);
}
@Test
void resolveResourceExisting() {
Resource expected = mock();
String file = "foo/2.3/foo.txt";
given(this.chain.resolveResource(this.exchange, file, this.locations)).willReturn(Mono.just(expected));
Resource actual = this.resolver
.resolveResource(this.exchange, file, this.locations, this.chain)
.block(TIMEOUT);
assertThat(actual).isEqualTo(expected);
verify(this.chain, times(1)).resolveResource(this.exchange, file, this.locations);
}
@Test
void resolveResourceNotFound() {
String file = "something/something.js";
given(this.chain.resolveResource(this.exchange, file, this.locations)).willReturn(Mono.empty());
Resource actual = this.resolver
.resolveResource(this.exchange, file, this.locations, this.chain)
.block(TIMEOUT);
assertThat(actual).isNull();
verify(this.chain, times(1)).resolveResource(this.exchange, file, this.locations);
verify(this.chain, never()).resolveResource(this.exchange, null, this.locations);
}
@Test
void resolveResourceWebJar() {
String file = "underscorejs/underscore.js";
given(this.chain.resolveResource(this.exchange, file, this.locations)).willReturn(Mono.empty());
Resource expected = mock();
String expectedPath = "underscorejs/1.8.3/underscore.js";
given(this.chain.resolveResource(this.exchange, expectedPath, this.locations))
.willReturn(Mono.just(expected));
Resource actual = this.resolver
.resolveResource(this.exchange, file, this.locations, this.chain)
.block(TIMEOUT);
assertThat(actual).isEqualTo(expected);
verify(this.chain, times(1)).resolveResource(this.exchange, file, this.locations);
}
}

View File

@ -39,6 +39,7 @@ dependencies {
optional("org.jetbrains.kotlinx:kotlinx-serialization-protobuf")
optional("org.reactivestreams:reactive-streams")
optional("org.webjars:webjars-locator-core")
optional("org.webjars:webjars-locator-lite")
testImplementation(testFixtures(project(":spring-beans")))
testImplementation(testFixtures(project(":spring-context")))
testImplementation(testFixtures(project(":spring-core")))

View File

@ -306,6 +306,7 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
}
}
@SuppressWarnings("removal")
private void parseResourceResolversTransformers(boolean isAutoRegistration,
ManagedList<Object> resourceResolvers, ManagedList<Object> resourceTransformers,
ParserContext context, Element element, @Nullable Object source) {

View File

@ -27,6 +27,7 @@ import org.springframework.util.ClassUtils;
import org.springframework.web.servlet.resource.CachingResourceResolver;
import org.springframework.web.servlet.resource.CachingResourceTransformer;
import org.springframework.web.servlet.resource.CssLinkResourceTransformer;
import org.springframework.web.servlet.resource.LiteWebJarsResourceResolver;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.web.servlet.resource.ResourceResolver;
import org.springframework.web.servlet.resource.ResourceTransformer;
@ -43,9 +44,12 @@ public class ResourceChainRegistration {
private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache";
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
private static final boolean isWebJarAssetLocatorPresent = ClassUtils.isPresent(
"org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader());
private static final boolean isWebJarVersionLocatorPresent = ClassUtils.isPresent(
"org.webjars.WebJarVersionLocator", ResourceChainRegistration.class.getClassLoader());
private final List<ResourceResolver> resolvers = new ArrayList<>(4);
@ -79,6 +83,7 @@ public class ResourceChainRegistration {
* @param resolver the resolver to add
* @return the current instance for chained method invocation
*/
@SuppressWarnings("removal")
public ResourceChainRegistration addResolver(ResourceResolver resolver) {
Assert.notNull(resolver, "The provided ResourceResolver should not be null");
this.resolvers.add(resolver);
@ -88,7 +93,7 @@ public class ResourceChainRegistration {
else if (resolver instanceof PathResourceResolver) {
this.hasPathResolver = true;
}
else if (resolver instanceof WebJarsResourceResolver) {
else if (resolver instanceof WebJarsResourceResolver || resolver instanceof LiteWebJarsResourceResolver) {
this.hasWebjarsResolver = true;
}
return this;
@ -108,10 +113,14 @@ public class ResourceChainRegistration {
return this;
}
@SuppressWarnings("removal")
protected List<ResourceResolver> getResourceResolvers() {
if (!this.hasPathResolver) {
List<ResourceResolver> result = new ArrayList<>(this.resolvers);
if (isWebJarsAssetLocatorPresent && !this.hasWebjarsResolver) {
if (isWebJarVersionLocatorPresent && !this.hasWebjarsResolver) {
result.add(new LiteWebJarsResourceResolver());
}
else if (isWebJarAssetLocatorPresent && !this.hasWebjarsResolver) {
result.add(new WebJarsResourceResolver());
}
result.add(new PathResourceResolver());

View File

@ -0,0 +1,112 @@
/*
* 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.servlet.resource;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.webjars.WebJarVersionLocator;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
/**
* A {@code ResourceResolver} that delegates to the chain to locate a resource and then
* attempts to find a matching versioned resource contained in a WebJar JAR file.
*
* <p>This allows WebJars.org users to write version agnostic paths in their templates,
* like {@code <script src="/webjars/jquery/jquery.min.js"/>}.
* This path will be resolved to the unique version {@code <script src="/webjars/jquery/1.2.0/jquery.min.js"/>},
* which is a better fit for HTTP caching and version management in applications.
*
* <p>This also resolves resources for version agnostic HTTP requests {@code "GET /jquery/jquery.min.js"}.
*
* <p>This resolver requires the {@code org.webjars:webjars-locator-lite} library
* on the classpath and is automatically registered if that library is present.
*
* @author Sebastien Deleuze
* @since 6.2
* @see org.springframework.web.servlet.config.annotation.ResourceChainRegistration
* @see <a href="https://www.webjars.org">webjars.org</a>
*/
public class LiteWebJarsResourceResolver extends AbstractResourceResolver {
private static final int WEBJARS_LOCATION_LENGTH = WebJarVersionLocator.WEBJARS_PATH_PREFIX.length() + 1;
private final WebJarVersionLocator webJarAssetLocator;
/**
* Create a {@code LiteWebJarsResourceResolver} with a default {@code WebJarVersionLocator} instance.
*/
public LiteWebJarsResourceResolver() {
this.webJarAssetLocator = new WebJarVersionLocator();
}
/**
* Create a {@code LiteWebJarsResourceResolver} with a custom {@code WebJarVersionLocator} instance,
* e.g. with a custom cache implementation.
*/
public LiteWebJarsResourceResolver(WebJarVersionLocator webJarAssetLocator) {
this.webJarAssetLocator = webJarAssetLocator;
}
@Override
@Nullable
protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath,
List<? extends Resource> locations, ResourceResolverChain chain) {
Resource resolved = chain.resolveResource(request, requestPath, locations);
if (resolved == null) {
String webJarResourcePath = findWebJarResourcePath(requestPath);
if (webJarResourcePath != null) {
return chain.resolveResource(request, webJarResourcePath, locations);
}
}
return resolved;
}
@Override
@Nullable
protected String resolveUrlPathInternal(String resourceUrlPath,
List<? extends Resource> locations, ResourceResolverChain chain) {
String path = chain.resolveUrlPath(resourceUrlPath, locations);
if (path == null) {
String webJarResourcePath = findWebJarResourcePath(resourceUrlPath);
if (webJarResourcePath != null) {
return chain.resolveUrlPath(webJarResourcePath, locations);
}
}
return path;
}
@Nullable
protected String findWebJarResourcePath(String path) {
int startOffset = (path.startsWith("/") ? 1 : 0);
int endOffset = path.indexOf('/', 1);
if (endOffset != -1) {
String webjar = path.substring(startOffset, endOffset);
String partialPath = path.substring(endOffset + 1);
String webJarPath = this.webJarAssetLocator.fullPath(webjar, partialPath);
if (webJarPath != null) {
return webJarPath.substring(WEBJARS_LOCATION_LENGTH);
}
}
return null;
}
}

View File

@ -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.
@ -42,10 +42,14 @@ import org.springframework.lang.Nullable;
* could slow down application startup.
*
* @author Brian Clozel
* @author Sebastien Deleuze
* @since 4.2
* @see org.springframework.web.servlet.config.annotation.ResourceChainRegistration
* @see <a href="https://www.webjars.org">webjars.org</a>
* @see LiteWebJarsResourceResolver
* @deprecated as of Spring Framework 6.2 in favor of {@link LiteWebJarsResourceResolver}
*/
@Deprecated(forRemoval = true)
public class WebJarsResourceResolver extends AbstractResourceResolver {
private static final String WEBJARS_LOCATION = "META-INF/resources/webjars/";

View File

@ -439,6 +439,7 @@ public class MvcNamespaceTests {
}
@Test
@SuppressWarnings("removal")
void testResourcesWithResolversTransformers() {
loadBeanDefinitions("mvc-config-resources-chain.xml");

View File

@ -31,12 +31,12 @@ import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.resource.CachingResourceResolver;
import org.springframework.web.servlet.resource.CachingResourceTransformer;
import org.springframework.web.servlet.resource.CssLinkResourceTransformer;
import org.springframework.web.servlet.resource.LiteWebJarsResourceResolver;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.springframework.web.servlet.resource.ResourceResolver;
import org.springframework.web.servlet.resource.ResourceTransformer;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import org.springframework.web.servlet.resource.WebJarsResourceResolver;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import org.springframework.web.testfixture.servlet.MockServletContext;
@ -49,6 +49,7 @@ import static org.mockito.Mockito.mock;
* Tests for {@link ResourceHandlerRegistry}.
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
*/
class ResourceHandlerRegistryTests {
@ -128,6 +129,7 @@ class ResourceHandlerRegistryTests {
}
@Test
@SuppressWarnings("removal")
void resourceChain() {
ResourceResolver mockResolver = mock();
ResourceTransformer mockTransformer = mock();
@ -138,7 +140,7 @@ class ResourceHandlerRegistryTests {
zero -> assertThat(zero).isInstanceOfSatisfying(CachingResourceResolver.class,
cachingResolver -> assertThat(cachingResolver.getCache()).isInstanceOf(ConcurrentMapCache.class)),
one -> assertThat(one).isEqualTo(mockResolver),
two -> assertThat(two).isInstanceOf(WebJarsResourceResolver.class),
two -> assertThat(two).isInstanceOf(LiteWebJarsResourceResolver.class),
three -> assertThat(three).isInstanceOf(PathResourceResolver.class));
assertThat(handler.getResourceTransformers()).satisfiesExactly(
zero -> assertThat(zero).isInstanceOf(CachingResourceTransformer.class),
@ -151,7 +153,7 @@ class ResourceHandlerRegistryTests {
ResourceHttpRequestHandler handler = getHandler("/resources/**");
assertThat(handler.getResourceResolvers()).hasExactlyElementsOfTypes(
WebJarsResourceResolver.class, PathResourceResolver.class);
LiteWebJarsResourceResolver.class, PathResourceResolver.class);
assertThat(handler.getResourceTransformers()).isEmpty();
}
@ -167,7 +169,7 @@ class ResourceHandlerRegistryTests {
assertThat(handler.getResourceResolvers()).satisfiesExactly(
zero -> assertThat(zero).isInstanceOf(CachingResourceResolver.class),
one -> assertThat(one).isSameAs(versionResolver),
two -> assertThat(two).isInstanceOf(WebJarsResourceResolver.class),
two -> assertThat(two).isInstanceOf(LiteWebJarsResourceResolver.class),
three -> assertThat(three).isInstanceOf(PathResourceResolver.class));
assertThat(handler.getResourceTransformers()).hasExactlyElementsOfTypes(
CachingResourceTransformer.class, CssLinkResourceTransformer.class);
@ -177,7 +179,7 @@ class ResourceHandlerRegistryTests {
void resourceChainWithOverrides() {
CachingResourceResolver cachingResolver = mock();
VersionResourceResolver versionResolver = mock();
WebJarsResourceResolver webjarsResolver = mock();
LiteWebJarsResourceResolver webjarsResolver = mock();
PathResourceResolver pathResourceResolver = new PathResourceResolver();
CachingResourceTransformer cachingTransformer = mock();
CssLinkResourceTransformer cssLinkTransformer = new CssLinkResourceTransformer();

View File

@ -0,0 +1,138 @@
/*
* 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.servlet.resource;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link LiteWebJarsResourceResolver}.
*
* @author Sebastien Deleuze
*/
class LiteWebJarsResourceResolverTests {
private List<Resource> locations = List.of(new ClassPathResource("/META-INF/resources/webjars"));
// for this to work, an actual WebJar must be on the test classpath
private LiteWebJarsResourceResolver resolver = new LiteWebJarsResourceResolver();
private ResourceResolverChain chain = mock();
private HttpServletRequest request = new MockHttpServletRequest();
@Test
void resolveUrlExisting() {
String file = "/foo/2.3/foo.txt";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(file);
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
assertThat(actual).isEqualTo(file);
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
}
@Test
void resolveUrlExistingNotInJarFile() {
String file = "foo/foo.txt";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
assertThat(actual).isNull();
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
verify(this.chain, never()).resolveUrlPath("foo/2.3/foo.txt", this.locations);
}
@Test
void resolveUrlWebJarResource() {
String file = "underscorejs/underscore.js";
String expected = "underscorejs/1.8.3/underscore.js";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
given(this.chain.resolveUrlPath(expected, this.locations)).willReturn(expected);
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
assertThat(actual).isEqualTo(expected);
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
}
@Test
void resolveUrlWebJarResourceNotFound() {
String file = "something/something.js";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
assertThat(actual).isNull();
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
verify(this.chain, never()).resolveUrlPath(null, this.locations);
}
@Test
void resolveResourceExisting() {
Resource expected = mock();
String file = "foo/2.3/foo.txt";
given(this.chain.resolveResource(this.request, file, this.locations)).willReturn(expected);
Resource actual = this.resolver.resolveResource(this.request, file, this.locations, this.chain);
assertThat(actual).isEqualTo(expected);
verify(this.chain, times(1)).resolveResource(this.request, file, this.locations);
}
@Test
void resolveResourceNotFound() {
String file = "something/something.js";
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
Resource actual = this.resolver.resolveResource(this.request, file, this.locations, this.chain);
assertThat(actual).isNull();
verify(this.chain, times(1)).resolveResource(this.request, file, this.locations);
verify(this.chain, never()).resolveResource(this.request, null, this.locations);
}
@Test
void resolveResourceWebJar() {
Resource expected = mock();
String file = "underscorejs/underscore.js";
String expectedPath = "underscorejs/1.8.3/underscore.js";
given(this.chain.resolveResource(this.request, expectedPath, this.locations)).willReturn(expected);
Resource actual = this.resolver.resolveResource(this.request, file, this.locations, this.chain);
assertThat(actual).isEqualTo(expected);
verify(this.chain, times(1)).resolveResource(this.request, file, this.locations);
}
}

View File

@ -38,6 +38,7 @@ import static org.mockito.Mockito.verify;
* @author Brian Clozel
* @author Sam Brannen
*/
@SuppressWarnings("removal")
class WebJarsResourceResolverTests {
private List<Resource> locations = List.of(new ClassPathResource("/META-INF/resources/webjars"));