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:
Brian Clozel
2015-05-19 15:58:27 +02:00
parent 876c9694c4
commit 9e5a33c1b3
8 changed files with 234 additions and 9 deletions

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
}
}