Add a ResourceResolver implementation for WebJars
Prior to this commit, WebJars users needed to use versioned links within templates for WebJars resources, such as `/jquery/1.2.0/jquery.js`. This can be rather cumbersome when updating libraries - all references in templates need to be updated. One could use version-less links in templates, but needed to add a specific MVC Handler that uses webjars.org's webjar-locator library. While this approach makes maintaing templates easier, this makes HTTP caching strategies less optimal. This commit adds a new WebJarsResourceResolver that search for resources located in WebJar locations. This ResourceResolver is automatically registered if the "org.webjars:webjars-locator" dependency is present. Registering WebJars resource handling can be done like this: ```java @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:META-INF/resources/webjars") .resourceChain(true) .addResolver(new WebJarsResourceResolver()); } ``` Issue: SPR-12323 polish
This commit is contained in:
parent
876c9694c4
commit
9e5a33c1b3
|
@ -895,6 +895,7 @@ project("spring-webmvc") {
|
|||
exclude group: "org.slf4j", module: "jcl-over-slf4j"
|
||||
exclude group: "org.springframework", module: "spring-web"
|
||||
}
|
||||
optional 'org.webjars:webjars-locator:0.22'
|
||||
testCompile(project(":spring-aop"))
|
||||
testCompile("rhino:js:1.7R1")
|
||||
testCompile("xmlunit:xmlunit:${xmlunitVersion}")
|
||||
|
@ -921,6 +922,7 @@ project("spring-webmvc") {
|
|||
testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}")
|
||||
testCompile("org.jruby:jruby:${jrubyVersion}")
|
||||
testCompile("org.python:jython-standalone:2.5.3")
|
||||
testCompile("org.webjars:underscorejs:1.8.2")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser;
|
|||
import org.springframework.beans.factory.xml.ParserContext;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCache;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.xml.DomUtils;
|
||||
import org.springframework.http.CacheControl;
|
||||
|
@ -51,6 +52,7 @@ import org.springframework.web.servlet.resource.ResourceTransformer;
|
|||
import org.springframework.web.servlet.resource.ResourceUrlProvider;
|
||||
import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;
|
||||
import org.springframework.web.servlet.resource.VersionResourceResolver;
|
||||
import org.springframework.web.servlet.resource.WebJarsResourceResolver;
|
||||
|
||||
/**
|
||||
* {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a
|
||||
|
@ -78,6 +80,9 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
|
|||
|
||||
private static final String RESOURCE_URL_PROVIDER = "mvcResourceUrlProvider";
|
||||
|
||||
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
|
||||
"org.webjars.WebJarAssetLocator", ResourcesBeanDefinitionParser.class.getClassLoader());
|
||||
|
||||
|
||||
@Override
|
||||
public BeanDefinition parse(Element element, ParserContext parserContext) {
|
||||
|
@ -302,6 +307,12 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
|
|||
}
|
||||
|
||||
if (isAutoRegistration) {
|
||||
if(isWebJarsAssetLocatorPresent) {
|
||||
RootBeanDefinition webJarsResolverDef = new RootBeanDefinition(WebJarsResourceResolver.class);
|
||||
webJarsResolverDef.setSource(source);
|
||||
webJarsResolverDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
resourceResolvers.add(webJarsResolverDef);
|
||||
}
|
||||
RootBeanDefinition pathResolverDef = new RootBeanDefinition(PathResourceResolver.class);
|
||||
pathResolverDef.setSource(source);
|
||||
pathResolverDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
* Copyright 2002-2015 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.
|
||||
|
@ -22,6 +22,7 @@ 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.servlet.resource.CachingResourceResolver;
|
||||
import org.springframework.web.servlet.resource.CachingResourceTransformer;
|
||||
import org.springframework.web.servlet.resource.CssLinkResourceTransformer;
|
||||
|
@ -29,6 +30,7 @@ import org.springframework.web.servlet.resource.PathResourceResolver;
|
|||
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;
|
||||
|
||||
/**
|
||||
* Assists with the registration of resource resolvers and transformers.
|
||||
|
@ -40,6 +42,9 @@ 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<ResourceResolver> resolvers = new ArrayList<ResourceResolver>(4);
|
||||
|
||||
private final List<ResourceTransformer> transformers = new ArrayList<ResourceTransformer>(4);
|
||||
|
@ -98,6 +103,9 @@ public class ResourceChainRegistration {
|
|||
protected List<ResourceResolver> getResourceResolvers() {
|
||||
if (!this.hasPathResolver) {
|
||||
List<ResourceResolver> result = new ArrayList<ResourceResolver>(this.resolvers);
|
||||
if(isWebJarsAssetLocatorPresent) {
|
||||
result.add(new WebJarsResourceResolver());
|
||||
}
|
||||
result.add(new PathResourceResolver());
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright 2002-2015 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.servlet.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.webjars.MultipleMatchesException;
|
||||
import org.webjars.WebJarAssetLocator;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* 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 WabJar users to use version-less paths in their templates, like {@code "/jquery/jquery.min.js"}
|
||||
* while this path is resolved to the unique version {@code "/jquery/1.2.0/jquery.min.js"}, which is a better fit
|
||||
* for HTTP caching and version management in applications.
|
||||
*
|
||||
* <p>This resolver requires the "org.webjars:webjars-locator" library on classpath, and is automatically
|
||||
* registered if that library is present.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.2
|
||||
* @see <a href="http://www.webjars.org">webjars.org</a>
|
||||
*/
|
||||
public class WebJarsResourceResolver extends AbstractResourceResolver {
|
||||
|
||||
private final static String WEBJARS_LOCATION = "META-INF/resources/webjars";
|
||||
|
||||
private final static int WEBJARS_LOCATION_LENGTH = WEBJARS_LOCATION.length();
|
||||
|
||||
private final WebJarAssetLocator webJarAssetLocator;
|
||||
|
||||
public WebJarsResourceResolver() {
|
||||
this.webJarAssetLocator = new WebJarAssetLocator();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
return chain.resolveResource(request, requestPath, locations);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String resolveUrlPathInternal(String resourceUrlPath,
|
||||
List<? extends Resource> locations, ResourceResolverChain chain) {
|
||||
|
||||
String path = chain.resolveUrlPath(resourceUrlPath, locations);
|
||||
if (path == null) {
|
||||
try {
|
||||
int startOffset = resourceUrlPath.startsWith("/") ? 1 : 0;
|
||||
int endOffset = resourceUrlPath.indexOf("/", 1);
|
||||
if (endOffset != -1) {
|
||||
String webjar = resourceUrlPath.substring(startOffset, endOffset);
|
||||
String partialPath = resourceUrlPath.substring(endOffset);
|
||||
String webJarPath = webJarAssetLocator.getFullPath(webjar, partialPath);
|
||||
return chain.resolveUrlPath(webJarPath.substring(WEBJARS_LOCATION_LENGTH), locations);
|
||||
}
|
||||
}
|
||||
catch (MultipleMatchesException ex) {
|
||||
logger.warn("WebJar version conflict for \"" + resourceUrlPath + "\"", ex);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("No WebJar resource found for \"" + resourceUrlPath + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
}
|
|
@ -119,6 +119,7 @@ import org.springframework.web.servlet.resource.ResourceTransformer;
|
|||
import org.springframework.web.servlet.resource.ResourceUrlProvider;
|
||||
import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;
|
||||
import org.springframework.web.servlet.resource.VersionResourceResolver;
|
||||
import org.springframework.web.servlet.resource.WebJarsResourceResolver;
|
||||
import org.springframework.web.servlet.theme.ThemeChangeInterceptor;
|
||||
import org.springframework.web.servlet.view.BeanNameViewResolver;
|
||||
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
|
||||
|
@ -409,10 +410,11 @@ public class MvcNamespaceTests {
|
|||
assertNotNull(handler);
|
||||
|
||||
List<ResourceResolver> resolvers = handler.getResourceResolvers();
|
||||
assertThat(resolvers, Matchers.hasSize(3));
|
||||
assertThat(resolvers, Matchers.hasSize(4));
|
||||
assertThat(resolvers.get(0), Matchers.instanceOf(CachingResourceResolver.class));
|
||||
assertThat(resolvers.get(1), Matchers.instanceOf(VersionResourceResolver.class));
|
||||
assertThat(resolvers.get(2), Matchers.instanceOf(PathResourceResolver.class));
|
||||
assertThat(resolvers.get(2), Matchers.instanceOf(WebJarsResourceResolver.class));
|
||||
assertThat(resolvers.get(3), Matchers.instanceOf(PathResourceResolver.class));
|
||||
|
||||
CachingResourceResolver cachingResolver = (CachingResourceResolver) resolvers.get(0);
|
||||
assertThat(cachingResolver.getCache(), Matchers.instanceOf(ConcurrentMapCache.class));
|
||||
|
|
|
@ -40,6 +40,7 @@ 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 static org.junit.Assert.*;
|
||||
|
||||
|
@ -125,12 +126,13 @@ public class ResourceHandlerRegistryTests {
|
|||
|
||||
ResourceHttpRequestHandler handler = getHandler("/resources/**");
|
||||
List<ResourceResolver> resolvers = handler.getResourceResolvers();
|
||||
assertThat(resolvers.toString(), resolvers, Matchers.hasSize(3));
|
||||
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(PathResourceResolver.class));
|
||||
assertThat(resolvers.get(2), Matchers.instanceOf(WebJarsResourceResolver.class));
|
||||
assertThat(resolvers.get(3), Matchers.instanceOf(PathResourceResolver.class));
|
||||
|
||||
List<ResourceTransformer> transformers = handler.getResourceTransformers();
|
||||
assertThat(transformers, Matchers.hasSize(2));
|
||||
|
@ -144,8 +146,9 @@ public class ResourceHandlerRegistryTests {
|
|||
|
||||
ResourceHttpRequestHandler handler = getHandler("/resources/**");
|
||||
List<ResourceResolver> resolvers = handler.getResourceResolvers();
|
||||
assertThat(resolvers, Matchers.hasSize(1));
|
||||
assertThat(resolvers.get(0), Matchers.instanceOf(PathResourceResolver.class));
|
||||
assertThat(resolvers, Matchers.hasSize(2));
|
||||
assertThat(resolvers.get(0), Matchers.instanceOf(WebJarsResourceResolver.class));
|
||||
assertThat(resolvers.get(1), Matchers.instanceOf(PathResourceResolver.class));
|
||||
|
||||
List<ResourceTransformer> transformers = handler.getResourceTransformers();
|
||||
assertThat(transformers, Matchers.hasSize(0));
|
||||
|
@ -162,10 +165,11 @@ public class ResourceHandlerRegistryTests {
|
|||
|
||||
ResourceHttpRequestHandler handler = getHandler("/resources/**");
|
||||
List<ResourceResolver> resolvers = handler.getResourceResolvers();
|
||||
assertThat(resolvers.toString(), resolvers, Matchers.hasSize(3));
|
||||
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(PathResourceResolver.class));
|
||||
assertThat(resolvers.get(2), Matchers.instanceOf(WebJarsResourceResolver.class));
|
||||
assertThat(resolvers.get(3), Matchers.instanceOf(PathResourceResolver.class));
|
||||
|
||||
List<ResourceTransformer> transformers = handler.getResourceTransformers();
|
||||
assertThat(transformers, Matchers.hasSize(3));
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright 2002-2015 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.servlet.resource;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.mockito.BDDMockito.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* Unit tests for
|
||||
* {@link org.springframework.web.servlet.resource.WebJarsResourceResolver}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class WebJarsResourceResolverTests {
|
||||
|
||||
private List<Resource> locations;
|
||||
|
||||
private WebJarsResourceResolver resolver;
|
||||
|
||||
private ResourceResolverChain chain;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
// for this to work, an actual WebJar must be on the test classpath
|
||||
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars/"));
|
||||
this.resolver = new WebJarsResourceResolver();
|
||||
this.chain = mock(ResourceResolverChain.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveUrlExisting() {
|
||||
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars/", getClass()));
|
||||
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);
|
||||
|
||||
assertEquals(file, actual);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveUrlExistingNotInJarFile() {
|
||||
this.locations = Collections.singletonList(new ClassPathResource("/META-INF/resources/webjars/", getClass()));
|
||||
String file = "/foo/foo.txt";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
|
||||
|
||||
assertNull(actual);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, never()).resolveUrlPath("/foo/2.3/foo.txt", this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveUrlWebJarResource() {
|
||||
String file = "/underscorejs/underscore.js";
|
||||
String expected = "/underscorejs/1.8.2/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);
|
||||
|
||||
assertEquals(expected, actual);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
verify(this.chain, times(1)).resolveUrlPath(expected, this.locations);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolverUrlWebJarResourceNotFound() {
|
||||
String file = "/something/something.js";
|
||||
given(this.chain.resolveUrlPath(file, this.locations)).willReturn(null);
|
||||
|
||||
String actual = this.resolver.resolveUrlPath(file, this.locations, this.chain);
|
||||
|
||||
assertNull(actual);
|
||||
verify(this.chain, times(1)).resolveUrlPath(file, this.locations);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Some text.
|
Loading…
Reference in New Issue