diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/AppCacheManifestTransformer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/AppCacheManifestTransformer.java index 24b4e719e91..e1ce1408722 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/AppCacheManifestTransformer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/AppCacheManifestTransformer.java @@ -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; 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. * *

This transformer: *

* - *

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

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()}. + *

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 { 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 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 processLine(LineInfo info, ServerWebExchange exchange, + private Mono 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 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 resourceMono = chain.getResolverChain() - .resolveResource(null, info.getLine(), Collections.singletonList(resource)); - - return Flux.zip(pathMono, resourceMono, LineOutput::new).next(); } - private static class LineGenerator implements Consumer> { + private static class LineInfoGenerator implements Consumer> { 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 sink) { if (this.scanner.hasNext()) { @@ -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 { 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 { } } - - 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); - } - } - } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java index 8bf8b71e3d3..e552e09d3f9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/resource/CssLinkResourceTransformer.java @@ -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; 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 { @Override - public Mono transform(ServerWebExchange exchange, Resource resource, + public Mono 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 transformContent(String cssContent, Resource resource, + ResourceTransformerChain chain, ServerWebExchange exchange) { + + List 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 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); }); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/AppCacheManifestTransformerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/AppCacheManifestTransformerTests.java index eed61a727b3..de99f3cd38a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/AppCacheManifestTransformerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/AppCacheManifestTransformerTests.java @@ -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")); } }