Browse Source

Resource transformers use AsynchronousFileChannel

Issue: SPR-15773
pull/1480/head
Rossen Stoyanchev 9 years ago
parent
commit
b0422d02dc
  1. 166
      spring-webflux/src/main/java/org/springframework/web/reactive/resource/AppCacheManifestTransformer.java
  2. 96
      spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java
  3. 2
      spring-webflux/src/test/java/org/springframework/web/reactive/resource/AppCacheManifestTransformerTests.java

166
spring-webflux/src/main/java/org/springframework/web/reactive/resource/AppCacheManifestTransformer.java

@ -18,12 +18,11 @@ package org.springframework.web.reactive.resource; @@ -18,12 +18,11 @@ package org.springframework.web.reactive.resource;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Scanner;
import java.util.function.Consumer;
@ -35,15 +34,17 @@ import reactor.core.publisher.Mono; @@ -35,15 +34,17 @@ import reactor.core.publisher.Mono;
import reactor.core.publisher.SynchronousSink;
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.lang.Nullable;
import org.springframework.util.DigestUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
/**
* A {@link ResourceTransformer} implementation that helps handling resources
* within HTML5 AppCache manifests for HTML5 offline applications.
* A {@link ResourceTransformer} HTML5 AppCache manifests.
*
* <p>This transformer:
* <ul>
@ -54,15 +55,11 @@ import org.springframework.web.server.ServerWebExchange; @@ -54,15 +55,11 @@ import org.springframework.web.server.ServerWebExchange;
* of the manifest in order to trigger an appcache reload in the browser.
* </ul>
*
* <p>All files that have the ".appcache" file extension, or the extension given in the constructor,
* will be transformed by this class. This hash is computed using the content of the appcache manifest
* and the content of the linked resources; so changing a resource linked in the manifest
* or the manifest itself should invalidate the browser cache.
*
* <p>In order to serve manifest files with the proper {@code "text/manifest"} content type,
* it is required to configure it with
* {@code requestedContentTypeResolverBuilder.mediaType("appcache", MediaType.valueOf("text/manifest")}
* in {@code WebFluxConfigurer.configureContentTypeResolver()}.
* <p>All files with an ".appcache" file extension (or the extension given
* to the constructor) will be transformed by this class. The hash is computed
* using the content of the appcache manifest so that changes in the manifest
* should invalidate the browser cache. This should also work with changes in
* referenced resources whose links are also versioned.
*
* @author Rossen Stoyanchev
* @author Brian Clozel
@ -107,70 +104,91 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport { @@ -107,70 +104,91 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport {
ResourceTransformerChain chain) {
return chain.transform(exchange, inputResource)
.flatMap(resource -> {
String name = resource.getFilename();
.flatMap(outputResource -> {
String name = outputResource.getFilename();
if (!this.fileExtension.equals(StringUtils.getFilenameExtension(name))) {
return Mono.just(resource);
}
String content = new String(getResourceBytes(resource), DEFAULT_CHARSET);
if (!content.startsWith(MANIFEST_HEADER)) {
if (logger.isTraceEnabled()) {
logger.trace("Manifest should start with 'CACHE MANIFEST', skip: " + resource);
}
return Mono.just(resource);
return Mono.just(outputResource);
}
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
return DataBufferUtils.read(outputResource, bufferFactory, StreamUtils.BUFFER_SIZE)
.reduce(DataBuffer::write)
.flatMap(dataBuffer -> {
CharBuffer charBuffer = DEFAULT_CHARSET.decode(dataBuffer.asByteBuffer());
DataBufferUtils.release(dataBuffer);
String content = charBuffer.toString();
return transform(content, outputResource, chain, exchange);
});
});
}
private Mono<? extends Resource> transform(String content, Resource resource,
ResourceTransformerChain chain, ServerWebExchange exchange) {
if (!content.startsWith(MANIFEST_HEADER)) {
if (logger.isTraceEnabled()) {
logger.trace("Manifest should start with 'CACHE MANIFEST', skip: " + resource);
}
return Mono.just(resource);
}
if (logger.isTraceEnabled()) {
logger.trace("Transforming resource: " + resource);
}
return Flux.generate(new LineInfoGenerator(content))
.concatMap(info -> processLine(info, exchange, resource, chain))
.reduce(new ByteArrayOutputStream(), (out, line) -> {
writeToByteArrayOutputStream(out, line + "\n");
return out;
})
.map(out -> {
String hash = DigestUtils.md5DigestAsHex(out.toByteArray());
writeToByteArrayOutputStream(out, "\n" + "# Hash: " + hash);
if (logger.isTraceEnabled()) {
logger.trace("Transforming resource: " + resource);
logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]");
}
return Flux.generate(new LineGenerator(content))
.concatMap(info -> processLine(info, exchange, resource, chain))
.collect(() -> new LineAggregator(resource, content), LineAggregator::add)
.map(LineAggregator::createResource);
return new TransformedResource(resource, out.toByteArray());
});
}
private static byte[] getResourceBytes(Resource resource) {
private static void writeToByteArrayOutputStream(ByteArrayOutputStream out, String toWrite) {
try {
return FileCopyUtils.copyToByteArray(resource.getInputStream());
byte[] bytes = toWrite.getBytes(DEFAULT_CHARSET);
out.write(bytes);
}
catch (IOException ex) {
throw Exceptions.propagate(ex);
}
}
private Mono<LineOutput> processLine(LineInfo info, ServerWebExchange exchange,
private Mono<String> processLine(LineInfo info, ServerWebExchange exchange,
Resource resource, ResourceTransformerChain chain) {
if (!info.isLink()) {
return Mono.just(new LineOutput(info.getLine(), null));
return Mono.just(info.getLine());
}
String link = toAbsolutePath(info.getLine(), exchange);
Mono<String> pathMono = resolveUrlPath(link, exchange, resource, chain)
return resolveUrlPath(link, exchange, resource, chain)
.doOnNext(path -> {
if (logger.isTraceEnabled()) {
logger.trace("Link modified: " + path + " (original: " + info.getLine() + ")");
}
});
Mono<Resource> resourceMono = chain.getResolverChain()
.resolveResource(null, info.getLine(), Collections.singletonList(resource));
return Flux.zip(pathMono, resourceMono, LineOutput::new).next();
}
private static class LineGenerator implements Consumer<SynchronousSink<LineInfo>> {
private static class LineInfoGenerator implements Consumer<SynchronousSink<LineInfo>> {
private final Scanner scanner;
@Nullable
private LineInfo previous;
public LineGenerator(String content) {
LineInfoGenerator(String content) {
this.scanner = new Scanner(content);
}
@Override
public void accept(SynchronousSink<LineInfo> sink) {
if (this.scanner.hasNext()) {
@ -194,12 +212,14 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport { @@ -194,12 +212,14 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport {
private final boolean link;
public LineInfo(String line, @Nullable LineInfo previousLine) {
LineInfo(String line, @Nullable LineInfo previousLine) {
this.line = line;
this.cacheSection = initCacheSectionFlag(line, previousLine);
this.link = iniLinkFlag(line, this.cacheSection);
}
private static boolean initCacheSectionFlag(String line, @Nullable LineInfo previousLine) {
if (MANIFEST_SECTION_HEADERS.contains(line.trim())) {
return line.trim().equals(CACHE_HEADER);
@ -221,6 +241,7 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport { @@ -221,6 +241,7 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport {
return (line.startsWith("//") || (index > 0 && !line.substring(0, index).contains("/")));
}
public String getLine() {
return this.line;
}
@ -234,65 +255,4 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport { @@ -234,65 +255,4 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport {
}
}
private static class LineOutput {
private final String line;
@Nullable
private final Resource resource;
public LineOutput(String line, @Nullable Resource resource) {
this.line = line;
this.resource = resource;
}
public String getLine() {
return this.line;
}
@Nullable
public Resource getResource() {
return this.resource;
}
}
private static class LineAggregator {
private final StringWriter writer = new StringWriter();
private final ByteArrayOutputStream baos;
private final Resource resource;
public LineAggregator(Resource resource, String content) {
this.resource = resource;
this.baos = new ByteArrayOutputStream(content.length());
}
public void add(LineOutput lineOutput) {
this.writer.write(lineOutput.getLine() + "\n");
try {
byte[] bytes = (lineOutput.getResource() != null ?
DigestUtils.md5Digest(getResourceBytes(lineOutput.getResource())) :
lineOutput.getLine().getBytes(DEFAULT_CHARSET));
this.baos.write(bytes);
}
catch (IOException ex) {
throw Exceptions.propagate(ex);
}
}
public TransformedResource createResource() {
String hash = DigestUtils.md5DigestAsHex(this.baos.toByteArray());
this.writer.write("\n" + "# Hash: " + hash);
if (logger.isTraceEnabled()) {
logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]");
}
byte[] bytes = this.writer.toString().getBytes(DEFAULT_CHARSET);
return new TransformedResource(this.resource, bytes);
}
}
}

96
spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java

@ -16,8 +16,8 @@ @@ -16,8 +16,8 @@
package org.springframework.web.reactive.resource;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
@ -29,13 +29,15 @@ import java.util.TreeSet; @@ -29,13 +29,15 @@ import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
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.lang.Nullable;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
@ -69,58 +71,62 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { @@ -69,58 +71,62 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport {
@Override
public Mono<Resource> transform(ServerWebExchange exchange, Resource resource,
public Mono<Resource> transform(ServerWebExchange exchange, Resource inputResource,
ResourceTransformerChain transformerChain) {
return transformerChain.transform(exchange, resource)
.flatMap(newResource -> {
String filename = newResource.getFilename();
return transformerChain.transform(exchange, inputResource)
.flatMap(ouptputResource -> {
String filename = ouptputResource.getFilename();
if (!"css".equals(StringUtils.getFilenameExtension(filename)) ||
resource instanceof GzipResourceResolver.GzippedResource) {
return Mono.just(newResource);
inputResource instanceof GzipResourceResolver.GzippedResource) {
return Mono.just(ouptputResource);
}
if (logger.isTraceEnabled()) {
logger.trace("Transforming resource: " + newResource);
logger.trace("Transforming resource: " + ouptputResource);
}
byte[] bytes;
try {
bytes = FileCopyUtils.copyToByteArray(newResource.getInputStream());
}
catch (IOException ex) {
return Mono.error(Exceptions.propagate(ex));
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
return DataBufferUtils.read(ouptputResource, bufferFactory, StreamUtils.BUFFER_SIZE)
.reduce(DataBuffer::write)
.flatMap(dataBuffer -> {
CharBuffer charBuffer = DEFAULT_CHARSET.decode(dataBuffer.asByteBuffer());
DataBufferUtils.release(dataBuffer);
String cssContent = charBuffer.toString();
return transformContent(cssContent, ouptputResource, transformerChain, exchange);
});
});
}
private Mono<? extends Resource> transformContent(String cssContent, Resource resource,
ResourceTransformerChain chain, ServerWebExchange exchange) {
List<ContentChunkInfo> contentChunkInfos = parseContent(cssContent);
if (contentChunkInfos.isEmpty()) {
if (logger.isTraceEnabled()) {
logger.trace("No links found.");
}
return Mono.just(resource);
}
return Flux.fromIterable(contentChunkInfos)
.concatMap(contentChunkInfo -> {
String contentChunk = contentChunkInfo.getContent(cssContent);
if (contentChunkInfo.isLink() && !hasScheme(contentChunk)) {
String link = toAbsolutePath(contentChunk, exchange);
return resolveUrlPath(link, exchange, resource, chain).defaultIfEmpty(contentChunk);
}
String cssContent = new String(bytes, DEFAULT_CHARSET);
List<ContentChunkInfo> contentChunkInfos = parseContent(cssContent);
if (contentChunkInfos.isEmpty()) {
if (logger.isTraceEnabled()) {
logger.trace("No links found.");
}
return Mono.just(newResource);
else {
return Mono.just(contentChunk);
}
return Flux.fromIterable(contentChunkInfos)
.concatMap(contentChunkInfo -> {
String segmentContent = contentChunkInfo.getContent(cssContent);
if (contentChunkInfo.isLink() && !hasScheme(segmentContent)) {
String link = toAbsolutePath(segmentContent, exchange);
return resolveUrlPath(link, exchange, newResource, transformerChain)
.defaultIfEmpty(segmentContent);
}
else {
return Mono.just(segmentContent);
}
})
.reduce(new StringWriter(), (writer, chunk) -> {
writer.write(chunk);
return writer;
})
.map(writer -> {
byte[] newContent = writer.toString().getBytes(DEFAULT_CHARSET);
return new TransformedResource(resource, newContent);
});
})
.reduce(new StringWriter(), (writer, chunk) -> {
writer.write(chunk);
return writer;
})
.map(writer -> {
byte[] newContent = writer.toString().getBytes(DEFAULT_CHARSET);
return new TransformedResource(resource, newContent);
});
}

2
spring-webflux/src/test/java/org/springframework/web/reactive/resource/AppCacheManifestTransformerTests.java

@ -134,7 +134,7 @@ public class AppCacheManifestTransformerTests { @@ -134,7 +134,7 @@ public class AppCacheManifestTransformerTests {
Matchers.containsString("http://example.org/image.png"));
assertThat("should generate fingerprint", content,
Matchers.containsString("# Hash: 4bf0338bcbeb0a5b3a4ec9ed8864107d"));
Matchers.containsString("# Hash: 8eefc904df3bd46537fa7bdbbc5ab9fb"));
}
}

Loading…
Cancel
Save