ContentVersionStrategy has non-blocking contract

Issue: SPR-15773
This commit is contained in:
Rossen Stoyanchev 2017-07-18 15:52:29 +02:00
parent efb5ba5a95
commit 57221ab15c
7 changed files with 60 additions and 48 deletions

View File

@ -43,11 +43,6 @@ public abstract class AbstractPrefixVersionStrategy implements VersionStrategy {
} }
public String getPrefix() {
return this.prefix;
}
@Override @Override
public String extractVersion(String requestPath) { public String extractVersion(String requestPath) {
return requestPath.startsWith(this.prefix) ? this.prefix : null; return requestPath.startsWith(this.prefix) ? this.prefix : null;

View File

@ -16,10 +16,15 @@
package org.springframework.web.reactive.resource; package org.springframework.web.reactive.resource;
import java.io.IOException; import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.util.DigestUtils; import org.springframework.util.DigestUtils;
import org.springframework.util.StreamUtils;
/** /**
* A {@code VersionStrategy} that calculates an Hex MD5 hashes from the content * A {@code VersionStrategy} that calculates an Hex MD5 hashes from the content
@ -33,15 +38,19 @@ import org.springframework.util.DigestUtils;
*/ */
public class ContentVersionStrategy extends AbstractFileNameVersionStrategy { public class ContentVersionStrategy extends AbstractFileNameVersionStrategy {
private static final DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
@Override @Override
public String getResourceVersion(Resource resource) { public Mono<String> getResourceVersion(Resource resource) {
try { return DataBufferUtils.read(resource, dataBufferFactory, StreamUtils.BUFFER_SIZE)
return DigestUtils.md5DigestAsHex(resource.getInputStream()); .reduce(DataBuffer::write)
} .map(buffer -> {
catch (IOException ex) { byte[] result = new byte[buffer.readableByteCount()];
throw new IllegalStateException("Failed to calculate hash for " + resource, ex); buffer.read(result);
} DataBufferUtils.release(buffer);
return DigestUtils.md5DigestAsHex(result);
});
} }
} }

View File

@ -16,6 +16,8 @@
package org.springframework.web.reactive.resource; package org.springframework.web.reactive.resource;
import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
/** /**
@ -33,6 +35,8 @@ import org.springframework.core.io.Resource;
*/ */
public class FixedVersionStrategy extends AbstractPrefixVersionStrategy { public class FixedVersionStrategy extends AbstractPrefixVersionStrategy {
private final Mono<String> versionMono;
/** /**
* Create a new FixedVersionStrategy with the given version string. * Create a new FixedVersionStrategy with the given version string.
@ -40,12 +44,13 @@ public class FixedVersionStrategy extends AbstractPrefixVersionStrategy {
*/ */
public FixedVersionStrategy(String version) { public FixedVersionStrategy(String version) {
super(version); super(version);
this.versionMono = Mono.just(version);
} }
@Override @Override
public String getResourceVersion(Resource resource) { public Mono<String> getResourceVersion(Resource resource) {
return getPrefix(); return this.versionMono;
} }
} }

View File

@ -172,36 +172,37 @@ public class VersionResourceResolver extends AbstractResourceResolver {
return Mono.empty(); return Mono.empty();
} }
String candidateVersion = versionStrategy.extractVersion(requestPath); String candidate = versionStrategy.extractVersion(requestPath);
if (StringUtils.isEmpty(candidateVersion)) { if (StringUtils.isEmpty(candidate)) {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("No version found in path \"" + requestPath + "\""); logger.trace("No version found in path \"" + requestPath + "\"");
} }
return Mono.empty(); return Mono.empty();
} }
String simplePath = versionStrategy.removeVersion(requestPath, candidateVersion); String simplePath = versionStrategy.removeVersion(requestPath, candidate);
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Extracted version from path, re-resolving without version: \"" + simplePath + "\""); logger.trace("Extracted version from path, re-resolving without version: \"" + simplePath + "\"");
} }
return chain.resolveResource(exchange, simplePath, locations) return chain.resolveResource(exchange, simplePath, locations)
.flatMap(baseResource -> { .filterWhen(resource -> versionStrategy.getResourceVersion(resource)
String actualVersion = versionStrategy.getResourceVersion(baseResource); .map(actual -> {
if (candidateVersion.equals(actualVersion)) { if (candidate.equals(actual)) {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Resource matches extracted version [" + candidateVersion + "]"); logger.trace("Resource matches extracted version [" + candidate + "]");
} }
return Mono.just(new FileNameVersionedResource(baseResource, candidateVersion)); return true;
} }
else { else {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Potential resource found for \"" + requestPath + "\", but version [" + logger.trace("Potential resource found for \"" + requestPath + "\", " +
candidateVersion + "] does not match"); "but version [" + candidate + "] does not match");
} }
return Mono.empty(); return false;
} }
}); }))
.map(resource -> new FileNameVersionedResource(resource, candidate));
} }
@Override @Override
@ -211,8 +212,8 @@ public class VersionResourceResolver extends AbstractResourceResolver {
return chain.resolveUrlPath(resourceUrlPath, locations) return chain.resolveUrlPath(resourceUrlPath, locations)
.flatMap(baseUrl -> { .flatMap(baseUrl -> {
if (StringUtils.hasText(baseUrl)) { if (StringUtils.hasText(baseUrl)) {
VersionStrategy versionStrategy = getStrategyForPath(resourceUrlPath); VersionStrategy strategy = getStrategyForPath(resourceUrlPath);
if (versionStrategy == null) { if (strategy == null) {
return Mono.just(baseUrl); return Mono.just(baseUrl);
} }
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
@ -220,13 +221,13 @@ public class VersionResourceResolver extends AbstractResourceResolver {
"for path \"" + resourceUrlPath + "\""); "for path \"" + resourceUrlPath + "\"");
} }
return chain.resolveResource(null, baseUrl, locations) return chain.resolveResource(null, baseUrl, locations)
.map(resource -> { .flatMap(resource -> strategy.getResourceVersion(resource)
String version = versionStrategy.getResourceVersion(resource); .map(version -> {
if (logger.isTraceEnabled()) { if (logger.isTraceEnabled()) {
logger.trace("Determined version [" + version + "] for " + resource); logger.trace("Determined version [" + version + "] for " + resource);
} }
return versionStrategy.addVersion(baseUrl, version); return strategy.addVersion(baseUrl, version);
}); }));
} }
return Mono.empty(); return Mono.empty();
}); });

View File

@ -16,6 +16,8 @@
package org.springframework.web.reactive.resource; package org.springframework.web.reactive.resource;
import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -58,8 +60,8 @@ public interface VersionStrategy {
/** /**
* Determine the version for the given resource. * Determine the version for the given resource.
* @param resource the resource to check * @param resource the resource to check
* @return the version (never {@code null}) * @return the resource version
*/ */
String getResourceVersion(Resource resource); Mono<String> getResourceVersion(Resource resource);
} }

View File

@ -66,7 +66,7 @@ public class ContentBasedVersionStrategyTests {
public void getResourceVersion() throws Exception { public void getResourceVersion() throws Exception {
Resource expected = new ClassPathResource("test/bar.css", getClass()); Resource expected = new ClassPathResource("test/bar.css", getClass());
String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream())); String hash = DigestUtils.md5DigestAsHex(FileCopyUtils.copyToByteArray(expected.getInputStream()));
assertEquals(hash, this.strategy.getResourceVersion(expected)); assertEquals(hash, this.strategy.getResourceVersion(expected).block());
} }
@Test @Test

View File

@ -145,7 +145,7 @@ public class VersionResourceResolverTests {
given(this.chain.resolveResource(null, file, this.locations)).willReturn(Mono.just(expected)); given(this.chain.resolveResource(null, file, this.locations)).willReturn(Mono.just(expected));
given(this.versionStrategy.extractVersion(versionFile)).willReturn(version); given(this.versionStrategy.extractVersion(versionFile)).willReturn(version);
given(this.versionStrategy.removeVersion(versionFile, version)).willReturn(file); given(this.versionStrategy.removeVersion(versionFile, version)).willReturn(file);
given(this.versionStrategy.getResourceVersion(expected)).willReturn("newer-version"); given(this.versionStrategy.getResourceVersion(expected)).willReturn(Mono.just("newer-version"));
this.resolver.setStrategyMap(Collections.singletonMap("/**", this.versionStrategy)); this.resolver.setStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
Resource actual = this.resolver Resource actual = this.resolver
@ -167,7 +167,7 @@ public class VersionResourceResolverTests {
given(this.chain.resolveResource(exchange, file, this.locations)).willReturn(Mono.just(expected)); given(this.chain.resolveResource(exchange, file, this.locations)).willReturn(Mono.just(expected));
given(this.versionStrategy.extractVersion(versionFile)).willReturn(version); given(this.versionStrategy.extractVersion(versionFile)).willReturn(version);
given(this.versionStrategy.removeVersion(versionFile, version)).willReturn(file); given(this.versionStrategy.removeVersion(versionFile, version)).willReturn(file);
given(this.versionStrategy.getResourceVersion(expected)).willReturn(version); given(this.versionStrategy.getResourceVersion(expected)).willReturn(Mono.just(version));
this.resolver.setStrategyMap(Collections.singletonMap("/**", this.versionStrategy)); this.resolver.setStrategyMap(Collections.singletonMap("/**", this.versionStrategy));
Resource actual = this.resolver Resource actual = this.resolver