Refactor VersionResourceResolver in strategies

Prior to this commit, one of the available strategies for resolving
resources was the PrefixResourceResolver. Reconsidering the core goal of
this resolver and the FingerprintResourceResolver, we found that the
true core feature is versioning static resources application-wide.

This commit refactors both Resolvers by:
* having only on VersionResourceResolver
* that resolver takes a mapping of paths -> VersionStrategy
* provided VersionStrategy implementations are ContentBasedVS
  (previously FingerprintRR), FixedVS (previously PrefixRR)

One can add a VersionResourceResolver like this:

  Map<String, VersionStrategy> versionStrategies = new HashMap<>();
  versionStrategies.put("/**/*.js", new PrefixVersionStrategy("prefix"));
  versionStrategies.put("/**", new ContentBasedVersionStrategy());

  VersionResourceResolver versionResolver = new VersionResourceResolver();
  versionResolver.setVersionStrategyMap(versionStrategies);

  List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>();
  resolvers.add(versionResolver);
  resolvers.add(new PathResourceResolver());

Issue: SPR-11871
This commit is contained in:
Brian Clozel
2014-06-24 18:16:53 +02:00
parent 18131bf611
commit 13c4a0396d
16 changed files with 853 additions and 416 deletions

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2002-2014 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.ArrayList;
import java.util.Arrays;
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;
import org.springframework.util.DigestUtils;
import org.springframework.util.FileCopyUtils;
import static org.junit.Assert.*;
/**
* Unit tests for {@link org.springframework.web.servlet.resource.ContentBasedVersionStrategy}
* @author Brian Clozel
*/
public class ContentBasedVersionStrategyTests {
private List<Resource> locations;
private ContentBasedVersionStrategy versionStrategy = new ContentBasedVersionStrategy();
private ResourceResolverChain chain;
@Before
public void setup() {
this.locations = new ArrayList<Resource>();
this.locations.add(new ClassPathResource("test/", getClass()));
this.locations.add(new ClassPathResource("testalternatepath/", getClass()));
VersionResourceResolver versionResourceResolver = new VersionResourceResolver();
versionResourceResolver.setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
this.chain = new DefaultResourceResolverChain(Arrays.asList(versionResourceResolver, new PathResourceResolver()));
}
@Test
public void extractVersionFromPath() throws Exception {
String hash = "7fbe76cdac6093784895bb4989203e5a";
String path = "font-awesome/css/font-awesome.min-" + hash + ".css";
assertEquals(hash, this.versionStrategy.extractVersionFromPath(path));
assertEquals("", this.versionStrategy.extractVersionFromPath("foo/bar.css"));
}
@Test
public void deleteVersionFromPath() throws Exception {
String file = "font-awesome/css/font-awesome.min%s%s.css";
String hash = "7fbe76cdac6093784895bb4989203e5a";
assertEquals(String.format(file, "", ""), this.versionStrategy.deleteVersionFromPath(String.format(file, "-", hash), hash));
assertEquals("", this.versionStrategy.extractVersionFromPath("foo/bar.css"));
}
@Test
public void resourceVersionMatches() throws Exception {
Resource expected = new ClassPathResource("test/bar.css", getClass());
String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream()));
String wrongHash = "wronghash";
assertTrue(this.versionStrategy.resourceVersionMatches(expected, hash));
assertFalse(this.versionStrategy.resourceVersionMatches(expected, wrongHash));
}
@Test
public void addVersionToUrl() throws Exception {
String file = "bar.css";
Resource expected = new ClassPathResource("test/" + file, getClass());
String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream()));
String path = "bar-" + hash + ".css";
String resultUrl = this.versionStrategy.addVersionToUrl(file, this.locations, this.chain);
assertEquals(path, resultUrl);
}
}

View File

@@ -18,7 +18,9 @@ package org.springframework.web.servlet.resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
@@ -47,8 +49,13 @@ public class CssLinkResourceTransformerTests {
@Before
public void setUp() {
Map<String, VersionStrategy> versionStrategyMap = new HashMap<>();
versionStrategyMap.put("/**", new ContentBasedVersionStrategy());
VersionResourceResolver versionResolver = new VersionResourceResolver();
versionResolver.setVersionStrategyMap(versionStrategyMap);
List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>();
resolvers.add(new FingerprintResourceResolver());
resolvers.add(versionResolver);
resolvers.add(new PathResourceResolver());
ResourceResolverChain resolverChain = new DefaultResourceResolverChain(resolvers);

View File

@@ -1,124 +0,0 @@
/*
* Copyright 2002-2013 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.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.DigestUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.ReflectionUtils;
import static org.junit.Assert.*;
/**
*
* @author Jeremy Grelle
*/
public class FingerprintResourceResolverTests {
private ResourceResolverChain chain;
private FingerprintResourceResolver resolver = new FingerprintResourceResolver();
private List<Resource> locations;
@Before
public void setUp() {
List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>();
resolvers.add(resolver);
resolvers.add(new PathResourceResolver());
this.chain = new DefaultResourceResolverChain(resolvers);
this.locations = new ArrayList<Resource>();
this.locations.add(new ClassPathResource("test/", getClass()));
this.locations.add(new ClassPathResource("testalternatepath/", getClass()));
}
@Test
public void resolveWithoutHash() throws Exception {
String file = "bar.css";
Resource expected = new ClassPathResource("test/" + file, getClass());
Resource actual = chain.resolveResource(null, file, locations);
assertEquals(expected, actual);
}
@Test
public void resolveWithHashNoMatch() throws Exception {
String file = "bogus-e36d2e05253c6c7085a91522ce43a0b4.css";
assertNull(chain.resolveResource(null, file, locations));
}
@Test
public void resolveStaticFingerprintedResource() throws Exception {
String file = "foo-e36d2e05253c6c7085a91522ce43a0b4.css";
Resource expected = new ClassPathResource("test/" + file, getClass());
Resource actual = chain.resolveResource(null, file, locations);
assertEquals(expected, actual);
}
@Test
public void resolveDynamicFingerprintedResource() throws Exception {
Resource expected = new ClassPathResource("test/bar.css", getClass());
String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream()));
String path = "/bar-" + hash + ".css";
Resource actual = chain.resolveResource(null, path, locations);
assertEquals(expected, actual);
}
@Test
public void resolveWithMultipleExtensions() throws Exception {
Resource expected = new ClassPathResource("test/bar.min.css", getClass());
String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream()));
String path = "/bar.min-" + hash + ".css";
Resource actual = chain.resolveResource(null, path, locations);
assertEquals(expected, actual);
}
@Test
public void resolveWithMultipleHyphens() throws Exception {
Resource expected = new ClassPathResource("test/foo-bar/foo-bar.css", getClass());
String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream()));
String path = "/foo-bar/foo-bar-" + hash + ".css";
Resource actual = chain.resolveResource(null, path, locations);
assertEquals(expected, actual);
}
@Test
public void extractHash() throws Exception {
String hash = "7fbe76cdac6093784895bb4989203e5a";
String path = "font-awesome/css/font-awesome.min-" + hash + ".css";
Method method = ReflectionUtils.findMethod(resolver.getClass(), "extractHash", String.class);
ReflectionUtils.makeAccessible(method);
String result = (String) ReflectionUtils.invokeMethod(method, resolver, path);
assertEquals(hash, result);
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2002-2014 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.Collections;
import java.util.concurrent.Callable;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import org.springframework.core.io.Resource;
/**
* Unit tests for {@link org.springframework.web.servlet.resource.FixedVersionStrategy}
* @author Brian Clozel
*/
public class FixedVersionStrategyTests {
private final String version = "1df341f";
private final String resourceId = "js/foo.js";
private FixedVersionStrategy versionStrategy;
@Before
public void setup() {
this.versionStrategy = new FixedVersionStrategy(this.version);
}
@Test(expected = IllegalArgumentException.class)
public void constructWithEmptyPrefixVersion() throws Exception {
FixedVersionStrategy versionStrategy = new FixedVersionStrategy(" ");
}
@Test(expected = IllegalArgumentException.class)
public void constructWithEmptyCallableVersion() throws Exception {
FixedVersionStrategy versionStrategy = new FixedVersionStrategy(
new Callable<String>() {
@Override
public String call() throws Exception {
return " ";
}
});
}
@Test
public void extractVersionFromPath() throws Exception {
assertEquals(this.version + "/", this.versionStrategy.extractVersionFromPath(this.version + "/" + this.resourceId));
assertEquals("", this.versionStrategy.extractVersionFromPath(this.resourceId));
}
@Test
public void deleteVersionFromPath() throws Exception {
assertEquals(this.resourceId,
this.versionStrategy.deleteVersionFromPath(this.version + "/" + this.resourceId, this.version + "/"));
}
@Test
public void addVersionToUrl() throws Exception {
assertEquals(this.version + "/" + this.resourceId,
this.versionStrategy.addVersionToUrl(this.resourceId, Collections.<Resource>emptyList(), null));
}
}

View File

@@ -19,7 +19,9 @@ package org.springframework.web.servlet.resource;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
import org.junit.Before;
@@ -68,9 +70,14 @@ public class GzipResourceResolverTests {
@Before
public void setUp() {
Map<String, VersionStrategy> versionStrategyMap = new HashMap<>();
versionStrategyMap.put("/**", new ContentBasedVersionStrategy());
VersionResourceResolver versionResolver = new VersionResourceResolver();
versionResolver.setVersionStrategyMap(versionStrategyMap);
List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>();
resolvers.add(new GzipResourceResolver());
resolvers.add(new FingerprintResourceResolver());
resolvers.add(versionResolver);
resolvers.add(new PathResourceResolver());
resolver = new DefaultResourceResolverChain(resolvers);
locations = new ArrayList<Resource>();

View File

@@ -1,73 +0,0 @@
/*
* Copyright 2002-2014 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.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import static org.junit.Assert.*;
/**
* Test fixture for {@link PrefixResourceResolver}
*
* @author Brian Clozel
* @author Sam Brannen
*/
public class PrefixResourceResolverTests {
private final List<? extends Resource> locations = Arrays.asList(new ClassPathResource("test/", getClass()));
private final String shaPrefix = "1df341f";
private ResourceResolverChain chain;
@Before
public void setUp() {
List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>();
resolvers.add(new PrefixResourceResolver(this.shaPrefix));
resolvers.add(new PathResourceResolver());
this.chain = new DefaultResourceResolverChain(resolvers);
}
@Test
public void resolveResource() {
String resourceId = "foo.css";
Resource expected = new ClassPathResource("test/foo.css", getClass());
Resource actual = this.chain.resolveResource(null, this.shaPrefix + "/" + resourceId, this.locations);
assertEquals(expected, actual);
}
@Test
public void resolveUrlPath() {
String resourceId = "/foo.css";
String url = this.shaPrefix + resourceId;
assertEquals(url, chain.resolveUrlPath(resourceId, locations));
}
@Test(expected = IllegalArgumentException.class)
public void constructWithEmptyPrefix() {
new PrefixResourceResolver(" ");
}
}

View File

@@ -32,6 +32,9 @@ import org.springframework.web.servlet.config.annotation.*;
import static org.junit.Assert.*;
import java.util.HashMap;
import java.util.Map;
/**
* Integration tests using {@link ResourceUrlEncodingFilter} and
@@ -111,9 +114,14 @@ public class ResourceUrlProviderJavaConfigTests {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
Map<String, VersionStrategy> versionStrategyMap = new HashMap<>();
versionStrategyMap.put("/**", new ContentBasedVersionStrategy());
VersionResourceResolver versionResolver = new VersionResourceResolver();
versionResolver.setVersionStrategyMap(versionStrategyMap);
registry.addResourceHandler("/resources/**")
.addResourceLocations("classpath:org/springframework/web/servlet/resource/test/")
.setResourceResolvers(new FingerprintResourceResolver(), new PathResourceResolver());
.setResourceResolvers(versionResolver, new PathResourceResolver());
}
}

View File

@@ -67,8 +67,13 @@ public class ResourceUrlProviderTests {
@Test
public void getFingerprintedResourceUrl() {
Map<String, VersionStrategy> versionStrategyMap = new HashMap<>();
versionStrategyMap.put("/**", new ContentBasedVersionStrategy());
VersionResourceResolver versionResolver = new VersionResourceResolver();
versionResolver.setVersionStrategyMap(versionStrategyMap);
List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>();
resolvers.add(new FingerprintResourceResolver());
resolvers.add(versionResolver);
resolvers.add(new PathResourceResolver());
this.handler.setResourceResolvers(resolvers);
initTranslator();

View File

@@ -0,0 +1,174 @@
/*
* Copyright 2002-2014 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.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Unit tests for {@link VersionResourceResolver}
*
* @author Brian Clozel
* @author Sam Brannen
*/
public class VersionResourceResolverTests {
private List<Resource> locations;
private VersionResourceResolver versionResourceResolver;
private ResourceResolverChain chain;
private VersionStrategy versionStrategy;
@Before
public void setup() {
this.locations = new ArrayList<Resource>();
this.locations.add(new ClassPathResource("test/", getClass()));
this.locations.add(new ClassPathResource("testalternatepath/", getClass()));
this.versionResourceResolver = new VersionResourceResolver();
this.chain = mock(ResourceResolverChain.class);
this.versionStrategy = mock(VersionStrategy.class);
}
@Test
public void resolveResourceInternalExisting() throws Exception {
String file = "bar.css";
Resource expected = new ClassPathResource("test/" + file, getClass());
when(this.chain.resolveResource(null, file, this.locations)).thenReturn(expected);
this.versionResourceResolver
.setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
Resource actual = this.versionResourceResolver.resolveResourceInternal(null, file, this.locations, this.chain);
assertEquals(expected, actual);
verify(this.chain, times(1)).resolveResource(null, file, this.locations);
verify(this.versionStrategy, never()).extractVersionFromPath(file);
}
@Test
public void resolveResourceInternalNoStrategy() throws Exception {
String file = "missing.css";
when(this.chain.resolveResource(null, file, this.locations)).thenReturn(null);
this.versionResourceResolver.setVersionStrategyMap(Collections.emptyMap());
Resource actual = this.versionResourceResolver.resolveResourceInternal(null, file, this.locations, this.chain);
assertNull(actual);
verify(this.chain, times(1)).resolveResource(null, file, this.locations);
}
@Test
public void resolveResourceInternalNoCandidateVersion() throws Exception {
String file = "bar.css";
when(this.chain.resolveResource(null, file, this.locations)).thenReturn(null);
when(this.versionStrategy.extractVersionFromPath(file)).thenReturn("");
this.versionResourceResolver
.setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
Resource actual = this.versionResourceResolver.resolveResourceInternal(null, file, this.locations, this.chain);
assertNull(actual);
verify(this.chain, times(1)).resolveResource(null, file, this.locations);
verify(this.versionStrategy, times(1)).extractVersionFromPath(file);
}
@Test
public void resolveResourceInternalNoBaseResource() throws Exception {
String versionFile = "bar-version.css";
String version = "version";
String file = "bar.css";
when(this.chain.resolveResource(null, versionFile, this.locations)).thenReturn(null);
when(this.chain.resolveResource(null, file, this.locations)).thenReturn(null);
when(this.versionStrategy.extractVersionFromPath(versionFile)).thenReturn(version);
when(this.versionStrategy.deleteVersionFromPath(versionFile, version)).thenReturn(file);
this.versionResourceResolver
.setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
Resource actual = this.versionResourceResolver.resolveResourceInternal(null, versionFile, this.locations, this.chain);
assertNull(actual);
verify(this.versionStrategy, times(1)).deleteVersionFromPath(versionFile, version);
}
@Test
public void resolveResourceInternalNoMatch() throws Exception {
String versionFile = "bar-version.css";
String version = "version";
String file = "bar.css";
Resource expected = new ClassPathResource("test/" + file, getClass());
when(this.chain.resolveResource(null, versionFile, this.locations)).thenReturn(null);
when(this.chain.resolveResource(null, file, this.locations)).thenReturn(expected);
when(this.versionStrategy.extractVersionFromPath(versionFile)).thenReturn(version);
when(this.versionStrategy.deleteVersionFromPath(versionFile, version)).thenReturn(file);
when(this.versionStrategy.resourceVersionMatches(expected, version)).thenReturn(false);
this.versionResourceResolver
.setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
Resource actual = this.versionResourceResolver.resolveResourceInternal(null, versionFile, this.locations, this.chain);
assertNull(actual);
verify(this.versionStrategy, times(1)).resourceVersionMatches(expected, version);
}
@Test
public void resolveResourceInternalMatch() throws Exception {
String versionFile = "bar-version.css";
String version = "version";
String file = "bar.css";
Resource expected = new ClassPathResource("test/" + file, getClass());
when(this.chain.resolveResource(null, versionFile, this.locations)).thenReturn(null);
when(this.chain.resolveResource(null, file, this.locations)).thenReturn(expected);
when(this.versionStrategy.extractVersionFromPath(versionFile)).thenReturn(version);
when(this.versionStrategy.deleteVersionFromPath(versionFile, version)).thenReturn(file);
when(this.versionStrategy.resourceVersionMatches(expected, version)).thenReturn(true);
this.versionResourceResolver
.setVersionStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
Resource actual = this.versionResourceResolver.resolveResourceInternal(null, versionFile, this.locations, this.chain);
assertEquals(expected, actual);
verify(this.versionStrategy, times(1)).resourceVersionMatches(expected, version);
}
@Test
public void findStrategy() throws Exception {
Map<String, VersionStrategy> strategies = new HashMap<>();
VersionStrategy jsStrategy = mock(VersionStrategy.class);
VersionStrategy catchAllStrategy = mock(VersionStrategy.class);
strategies.put("/**", catchAllStrategy);
strategies.put("/**/*.js", jsStrategy);
this.versionResourceResolver.setVersionStrategyMap(strategies);
assertEquals(catchAllStrategy, this.versionResourceResolver.findStrategy("foo.css"));
assertEquals(catchAllStrategy, this.versionResourceResolver.findStrategy("foo-js.css"));
assertEquals(jsStrategy, this.versionResourceResolver.findStrategy("foo.js"));
assertEquals(jsStrategy, this.versionResourceResolver.findStrategy("bar/foo.js"));
}
}