diff --git a/build.gradle b/build.gradle index d56a602281..c17f5bd3c1 100644 --- a/build.gradle +++ b/build.gradle @@ -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") } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java index 9d00019646..02fbde8922 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java @@ -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); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java index 7749491f94..fea0fd8058 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java @@ -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 resolvers = new ArrayList(4); private final List transformers = new ArrayList(4); @@ -98,6 +103,9 @@ public class ResourceChainRegistration { protected List getResourceResolvers() { if (!this.hasPathResolver) { List result = new ArrayList(this.resolvers); + if(isWebJarsAssetLocatorPresent) { + result.add(new WebJarsResourceResolver()); + } result.add(new PathResourceResolver()); return result; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java new file mode 100644 index 0000000000..206d3fc271 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java @@ -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. + * + *

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. + * + *

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 webjars.org + */ +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 locations, ResourceResolverChain chain) { + + return chain.resolveResource(request, requestPath, locations); + } + + @Override + protected String resolveUrlPathInternal(String resourceUrlPath, + List 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; + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index fa3bc94ddf..92032a3196 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -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 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)); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java index 101a3135e1..d1d0df7a44 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistryTests.java @@ -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 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 transformers = handler.getResourceTransformers(); assertThat(transformers, Matchers.hasSize(2)); @@ -144,8 +146,9 @@ public class ResourceHandlerRegistryTests { ResourceHttpRequestHandler handler = getHandler("/resources/**"); List 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 transformers = handler.getResourceTransformers(); assertThat(transformers, Matchers.hasSize(0)); @@ -162,10 +165,11 @@ public class ResourceHandlerRegistryTests { ResourceHttpRequestHandler handler = getHandler("/resources/**"); List 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 transformers = handler.getResourceTransformers(); assertThat(transformers, Matchers.hasSize(3)); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/WebJarsResourceResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/WebJarsResourceResolverTests.java new file mode 100644 index 0000000000..3af4aea72c --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/resource/WebJarsResourceResolverTests.java @@ -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 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); + } + +} diff --git a/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/META-INF/resources/webjars/foo/2.3/foo.txt b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/META-INF/resources/webjars/foo/2.3/foo.txt new file mode 100644 index 0000000000..a9ed77c8dd --- /dev/null +++ b/spring-webmvc/src/test/resources/org/springframework/web/servlet/resource/META-INF/resources/webjars/foo/2.3/foo.txt @@ -0,0 +1 @@ +Some text. \ No newline at end of file