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:
*
@@ -54,15 +55,11 @@ import org.springframework.web.server.ServerWebExchange;
* of the manifest in order to trigger an appcache reload in the browser.
*
*
- * 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 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 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 extends Resource> 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"));
}
}