Skip to content

Commit b0422d0

Browse files
committed
Resource transformers use AsynchronousFileChannel
Issue: SPR-15773
1 parent bca5a36 commit b0422d0

File tree

3 files changed

+115
-149
lines changed

3 files changed

+115
-149
lines changed

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

Lines changed: 63 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,11 @@
1818

1919
import java.io.ByteArrayOutputStream;
2020
import java.io.IOException;
21-
import java.io.StringWriter;
21+
import java.nio.CharBuffer;
2222
import java.nio.charset.Charset;
2323
import java.nio.charset.StandardCharsets;
2424
import java.util.Arrays;
2525
import java.util.Collection;
26-
import java.util.Collections;
2726
import java.util.Scanner;
2827
import java.util.function.Consumer;
2928

@@ -35,15 +34,17 @@
3534
import reactor.core.publisher.SynchronousSink;
3635

3736
import org.springframework.core.io.Resource;
37+
import org.springframework.core.io.buffer.DataBuffer;
38+
import org.springframework.core.io.buffer.DataBufferFactory;
39+
import org.springframework.core.io.buffer.DataBufferUtils;
3840
import org.springframework.lang.Nullable;
3941
import org.springframework.util.DigestUtils;
40-
import org.springframework.util.FileCopyUtils;
42+
import org.springframework.util.StreamUtils;
4143
import org.springframework.util.StringUtils;
4244
import org.springframework.web.server.ServerWebExchange;
4345

4446
/**
45-
* A {@link ResourceTransformer} implementation that helps handling resources
46-
* within HTML5 AppCache manifests for HTML5 offline applications.
47+
* A {@link ResourceTransformer} HTML5 AppCache manifests.
4748
*
4849
* <p>This transformer:
4950
* <ul>
@@ -54,15 +55,11 @@
5455
* of the manifest in order to trigger an appcache reload in the browser.
5556
* </ul>
5657
*
57-
* <p>All files that have the ".appcache" file extension, or the extension given in the constructor,
58-
* will be transformed by this class. This hash is computed using the content of the appcache manifest
59-
* and the content of the linked resources; so changing a resource linked in the manifest
60-
* or the manifest itself should invalidate the browser cache.
61-
*
62-
* <p>In order to serve manifest files with the proper {@code "text/manifest"} content type,
63-
* it is required to configure it with
64-
* {@code requestedContentTypeResolverBuilder.mediaType("appcache", MediaType.valueOf("text/manifest")}
65-
* in {@code WebFluxConfigurer.configureContentTypeResolver()}.
58+
* <p>All files with an ".appcache" file extension (or the extension given
59+
* to the constructor) will be transformed by this class. The hash is computed
60+
* using the content of the appcache manifest so that changes in the manifest
61+
* should invalidate the browser cache. This should also work with changes in
62+
* referenced resources whose links are also versioned.
6663
*
6764
* @author Rossen Stoyanchev
6865
* @author Brian Clozel
@@ -107,70 +104,91 @@ public Mono<Resource> transform(ServerWebExchange exchange, Resource inputResour
107104
ResourceTransformerChain chain) {
108105

109106
return chain.transform(exchange, inputResource)
110-
.flatMap(resource -> {
111-
String name = resource.getFilename();
107+
.flatMap(outputResource -> {
108+
String name = outputResource.getFilename();
112109
if (!this.fileExtension.equals(StringUtils.getFilenameExtension(name))) {
113-
return Mono.just(resource);
114-
}
115-
String content = new String(getResourceBytes(resource), DEFAULT_CHARSET);
116-
if (!content.startsWith(MANIFEST_HEADER)) {
117-
if (logger.isTraceEnabled()) {
118-
logger.trace("Manifest should start with 'CACHE MANIFEST', skip: " + resource);
119-
}
120-
return Mono.just(resource);
110+
return Mono.just(outputResource);
121111
}
112+
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
113+
return DataBufferUtils.read(outputResource, bufferFactory, StreamUtils.BUFFER_SIZE)
114+
.reduce(DataBuffer::write)
115+
.flatMap(dataBuffer -> {
116+
CharBuffer charBuffer = DEFAULT_CHARSET.decode(dataBuffer.asByteBuffer());
117+
DataBufferUtils.release(dataBuffer);
118+
String content = charBuffer.toString();
119+
return transform(content, outputResource, chain, exchange);
120+
});
121+
});
122+
}
123+
124+
private Mono<? extends Resource> transform(String content, Resource resource,
125+
ResourceTransformerChain chain, ServerWebExchange exchange) {
126+
127+
if (!content.startsWith(MANIFEST_HEADER)) {
128+
if (logger.isTraceEnabled()) {
129+
logger.trace("Manifest should start with 'CACHE MANIFEST', skip: " + resource);
130+
}
131+
return Mono.just(resource);
132+
}
133+
if (logger.isTraceEnabled()) {
134+
logger.trace("Transforming resource: " + resource);
135+
}
136+
return Flux.generate(new LineInfoGenerator(content))
137+
.concatMap(info -> processLine(info, exchange, resource, chain))
138+
.reduce(new ByteArrayOutputStream(), (out, line) -> {
139+
writeToByteArrayOutputStream(out, line + "\n");
140+
return out;
141+
})
142+
.map(out -> {
143+
String hash = DigestUtils.md5DigestAsHex(out.toByteArray());
144+
writeToByteArrayOutputStream(out, "\n" + "# Hash: " + hash);
122145
if (logger.isTraceEnabled()) {
123-
logger.trace("Transforming resource: " + resource);
146+
logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]");
124147
}
125-
return Flux.generate(new LineGenerator(content))
126-
.concatMap(info -> processLine(info, exchange, resource, chain))
127-
.collect(() -> new LineAggregator(resource, content), LineAggregator::add)
128-
.map(LineAggregator::createResource);
148+
return new TransformedResource(resource, out.toByteArray());
129149
});
130150
}
131151

132-
private static byte[] getResourceBytes(Resource resource) {
152+
private static void writeToByteArrayOutputStream(ByteArrayOutputStream out, String toWrite) {
133153
try {
134-
return FileCopyUtils.copyToByteArray(resource.getInputStream());
154+
byte[] bytes = toWrite.getBytes(DEFAULT_CHARSET);
155+
out.write(bytes);
135156
}
136157
catch (IOException ex) {
137158
throw Exceptions.propagate(ex);
138159
}
139160
}
140161

141-
private Mono<LineOutput> processLine(LineInfo info, ServerWebExchange exchange,
162+
private Mono<String> processLine(LineInfo info, ServerWebExchange exchange,
142163
Resource resource, ResourceTransformerChain chain) {
143164

144165
if (!info.isLink()) {
145-
return Mono.just(new LineOutput(info.getLine(), null));
166+
return Mono.just(info.getLine());
146167
}
147168

148169
String link = toAbsolutePath(info.getLine(), exchange);
149-
Mono<String> pathMono = resolveUrlPath(link, exchange, resource, chain)
170+
return resolveUrlPath(link, exchange, resource, chain)
150171
.doOnNext(path -> {
151172
if (logger.isTraceEnabled()) {
152173
logger.trace("Link modified: " + path + " (original: " + info.getLine() + ")");
153174
}
154175
});
155-
156-
Mono<Resource> resourceMono = chain.getResolverChain()
157-
.resolveResource(null, info.getLine(), Collections.singletonList(resource));
158-
159-
return Flux.zip(pathMono, resourceMono, LineOutput::new).next();
160176
}
161177

162178

163-
private static class LineGenerator implements Consumer<SynchronousSink<LineInfo>> {
179+
private static class LineInfoGenerator implements Consumer<SynchronousSink<LineInfo>> {
164180

165181
private final Scanner scanner;
166182

167183
@Nullable
168184
private LineInfo previous;
169185

170-
public LineGenerator(String content) {
186+
187+
LineInfoGenerator(String content) {
171188
this.scanner = new Scanner(content);
172189
}
173190

191+
174192
@Override
175193
public void accept(SynchronousSink<LineInfo> sink) {
176194
if (this.scanner.hasNext()) {
@@ -194,12 +212,14 @@ private static class LineInfo {
194212

195213
private final boolean link;
196214

197-
public LineInfo(String line, @Nullable LineInfo previousLine) {
215+
216+
LineInfo(String line, @Nullable LineInfo previousLine) {
198217
this.line = line;
199218
this.cacheSection = initCacheSectionFlag(line, previousLine);
200219
this.link = iniLinkFlag(line, this.cacheSection);
201220
}
202221

222+
203223
private static boolean initCacheSectionFlag(String line, @Nullable LineInfo previousLine) {
204224
if (MANIFEST_SECTION_HEADERS.contains(line.trim())) {
205225
return line.trim().equals(CACHE_HEADER);
@@ -221,6 +241,7 @@ private static boolean hasScheme(String line) {
221241
return (line.startsWith("//") || (index > 0 && !line.substring(0, index).contains("/")));
222242
}
223243

244+
224245
public String getLine() {
225246
return this.line;
226247
}
@@ -234,65 +255,4 @@ public boolean isLink() {
234255
}
235256
}
236257

237-
238-
private static class LineOutput {
239-
240-
private final String line;
241-
242-
@Nullable
243-
private final Resource resource;
244-
245-
public LineOutput(String line, @Nullable Resource resource) {
246-
this.line = line;
247-
this.resource = resource;
248-
}
249-
250-
public String getLine() {
251-
return this.line;
252-
}
253-
254-
@Nullable
255-
public Resource getResource() {
256-
return this.resource;
257-
}
258-
}
259-
260-
261-
private static class LineAggregator {
262-
263-
private final StringWriter writer = new StringWriter();
264-
265-
private final ByteArrayOutputStream baos;
266-
267-
private final Resource resource;
268-
269-
public LineAggregator(Resource resource, String content) {
270-
this.resource = resource;
271-
this.baos = new ByteArrayOutputStream(content.length());
272-
}
273-
274-
public void add(LineOutput lineOutput) {
275-
this.writer.write(lineOutput.getLine() + "\n");
276-
try {
277-
byte[] bytes = (lineOutput.getResource() != null ?
278-
DigestUtils.md5Digest(getResourceBytes(lineOutput.getResource())) :
279-
lineOutput.getLine().getBytes(DEFAULT_CHARSET));
280-
this.baos.write(bytes);
281-
}
282-
catch (IOException ex) {
283-
throw Exceptions.propagate(ex);
284-
}
285-
}
286-
287-
public TransformedResource createResource() {
288-
String hash = DigestUtils.md5DigestAsHex(this.baos.toByteArray());
289-
this.writer.write("\n" + "# Hash: " + hash);
290-
if (logger.isTraceEnabled()) {
291-
logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]");
292-
}
293-
byte[] bytes = this.writer.toString().getBytes(DEFAULT_CHARSET);
294-
return new TransformedResource(this.resource, bytes);
295-
}
296-
}
297-
298258
}

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

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
package org.springframework.web.reactive.resource;
1818

19-
import java.io.IOException;
2019
import java.io.StringWriter;
20+
import java.nio.CharBuffer;
2121
import java.nio.charset.Charset;
2222
import java.nio.charset.StandardCharsets;
2323
import java.util.ArrayList;
@@ -29,13 +29,15 @@
2929

3030
import org.apache.commons.logging.Log;
3131
import org.apache.commons.logging.LogFactory;
32-
import reactor.core.Exceptions;
3332
import reactor.core.publisher.Flux;
3433
import reactor.core.publisher.Mono;
3534

3635
import org.springframework.core.io.Resource;
36+
import org.springframework.core.io.buffer.DataBuffer;
37+
import org.springframework.core.io.buffer.DataBufferFactory;
38+
import org.springframework.core.io.buffer.DataBufferUtils;
3739
import org.springframework.lang.Nullable;
38-
import org.springframework.util.FileCopyUtils;
40+
import org.springframework.util.StreamUtils;
3941
import org.springframework.util.StringUtils;
4042
import org.springframework.web.server.ServerWebExchange;
4143

@@ -69,58 +71,62 @@ public CssLinkResourceTransformer() {
6971

7072

7173
@Override
72-
public Mono<Resource> transform(ServerWebExchange exchange, Resource resource,
74+
public Mono<Resource> transform(ServerWebExchange exchange, Resource inputResource,
7375
ResourceTransformerChain transformerChain) {
7476

75-
return transformerChain.transform(exchange, resource)
76-
.flatMap(newResource -> {
77-
String filename = newResource.getFilename();
77+
return transformerChain.transform(exchange, inputResource)
78+
.flatMap(ouptputResource -> {
79+
String filename = ouptputResource.getFilename();
7880
if (!"css".equals(StringUtils.getFilenameExtension(filename)) ||
79-
resource instanceof GzipResourceResolver.GzippedResource) {
80-
return Mono.just(newResource);
81+
inputResource instanceof GzipResourceResolver.GzippedResource) {
82+
return Mono.just(ouptputResource);
8183
}
8284

8385
if (logger.isTraceEnabled()) {
84-
logger.trace("Transforming resource: " + newResource);
86+
logger.trace("Transforming resource: " + ouptputResource);
8587
}
8688

87-
byte[] bytes;
88-
try {
89-
bytes = FileCopyUtils.copyToByteArray(newResource.getInputStream());
90-
}
91-
catch (IOException ex) {
92-
return Mono.error(Exceptions.propagate(ex));
89+
DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory();
90+
return DataBufferUtils.read(ouptputResource, bufferFactory, StreamUtils.BUFFER_SIZE)
91+
.reduce(DataBuffer::write)
92+
.flatMap(dataBuffer -> {
93+
CharBuffer charBuffer = DEFAULT_CHARSET.decode(dataBuffer.asByteBuffer());
94+
DataBufferUtils.release(dataBuffer);
95+
String cssContent = charBuffer.toString();
96+
return transformContent(cssContent, ouptputResource, transformerChain, exchange);
97+
});
98+
});
99+
}
100+
101+
private Mono<? extends Resource> transformContent(String cssContent, Resource resource,
102+
ResourceTransformerChain chain, ServerWebExchange exchange) {
103+
104+
List<ContentChunkInfo> contentChunkInfos = parseContent(cssContent);
105+
if (contentChunkInfos.isEmpty()) {
106+
if (logger.isTraceEnabled()) {
107+
logger.trace("No links found.");
108+
}
109+
return Mono.just(resource);
110+
}
111+
112+
return Flux.fromIterable(contentChunkInfos)
113+
.concatMap(contentChunkInfo -> {
114+
String contentChunk = contentChunkInfo.getContent(cssContent);
115+
if (contentChunkInfo.isLink() && !hasScheme(contentChunk)) {
116+
String link = toAbsolutePath(contentChunk, exchange);
117+
return resolveUrlPath(link, exchange, resource, chain).defaultIfEmpty(contentChunk);
93118
}
94-
String cssContent = new String(bytes, DEFAULT_CHARSET);
95-
List<ContentChunkInfo> contentChunkInfos = parseContent(cssContent);
96-
97-
if (contentChunkInfos.isEmpty()) {
98-
if (logger.isTraceEnabled()) {
99-
logger.trace("No links found.");
100-
}
101-
return Mono.just(newResource);
119+
else {
120+
return Mono.just(contentChunk);
102121
}
103-
104-
return Flux.fromIterable(contentChunkInfos)
105-
.concatMap(contentChunkInfo -> {
106-
String segmentContent = contentChunkInfo.getContent(cssContent);
107-
if (contentChunkInfo.isLink() && !hasScheme(segmentContent)) {
108-
String link = toAbsolutePath(segmentContent, exchange);
109-
return resolveUrlPath(link, exchange, newResource, transformerChain)
110-
.defaultIfEmpty(segmentContent);
111-
}
112-
else {
113-
return Mono.just(segmentContent);
114-
}
115-
})
116-
.reduce(new StringWriter(), (writer, chunk) -> {
117-
writer.write(chunk);
118-
return writer;
119-
})
120-
.map(writer -> {
121-
byte[] newContent = writer.toString().getBytes(DEFAULT_CHARSET);
122-
return new TransformedResource(resource, newContent);
123-
});
122+
})
123+
.reduce(new StringWriter(), (writer, chunk) -> {
124+
writer.write(chunk);
125+
return writer;
126+
})
127+
.map(writer -> {
128+
byte[] newContent = writer.toString().getBytes(DEFAULT_CHARSET);
129+
return new TransformedResource(resource, newContent);
124130
});
125131
}
126132

0 commit comments

Comments
 (0)