Skip to content

Commit 33d9074

Browse files
committed
Asynchronous ResourceResolver + ResourceTransformer
Issue: SPR-14521
1 parent f592599 commit 33d9074

30 files changed

+739
-543
lines changed

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.apache.commons.logging.Log;
2222
import org.apache.commons.logging.LogFactory;
23+
import reactor.core.publisher.Mono;
2324

2425
import org.springframework.core.io.Resource;
2526
import org.springframework.web.server.ServerWebExchange;
@@ -36,7 +37,7 @@ public abstract class AbstractResourceResolver implements ResourceResolver {
3637

3738

3839
@Override
39-
public Resource resolveResource(ServerWebExchange exchange, String requestPath,
40+
public Mono<Resource> resolveResource(ServerWebExchange exchange, String requestPath,
4041
List<? extends Resource> locations, ResourceResolverChain chain) {
4142

4243
if (logger.isTraceEnabled()) {
@@ -46,7 +47,7 @@ public Resource resolveResource(ServerWebExchange exchange, String requestPath,
4647
}
4748

4849
@Override
49-
public String resolveUrlPath(String resourceUrlPath, List<? extends Resource> locations,
50+
public Mono<String> resolveUrlPath(String resourceUrlPath, List<? extends Resource> locations,
5051
ResourceResolverChain chain) {
5152

5253
if (logger.isTraceEnabled()) {
@@ -57,10 +58,10 @@ public String resolveUrlPath(String resourceUrlPath, List<? extends Resource> lo
5758
}
5859

5960

60-
protected abstract Resource resolveResourceInternal(ServerWebExchange exchange, String requestPath,
61-
List<? extends Resource> locations, ResourceResolverChain chain);
61+
protected abstract Mono<Resource> resolveResourceInternal(ServerWebExchange exchange,
62+
String requestPath, List<? extends Resource> locations, ResourceResolverChain chain);
6263

63-
protected abstract String resolveUrlPathInternal(String resourceUrlPath,
64+
protected abstract Mono<String> resolveUrlPathInternal(String resourceUrlPath,
6465
List<? extends Resource> locations, ResourceResolverChain chain);
6566

6667
}

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

Lines changed: 156 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@
2121
import java.io.StringWriter;
2222
import java.nio.charset.Charset;
2323
import java.nio.charset.StandardCharsets;
24+
import java.util.Arrays;
25+
import java.util.Collection;
2426
import java.util.Collections;
25-
import java.util.HashMap;
26-
import java.util.Map;
2727
import java.util.Scanner;
28+
import java.util.function.Consumer;
2829

2930
import org.apache.commons.logging.Log;
3031
import org.apache.commons.logging.LogFactory;
32+
import reactor.core.Exceptions;
33+
import reactor.core.publisher.Flux;
34+
import reactor.core.publisher.Mono;
35+
import reactor.core.publisher.SynchronousSink;
3136

3237
import org.springframework.core.io.Resource;
3338
import org.springframework.util.DigestUtils;
@@ -62,15 +67,18 @@
6267
*/
6368
public class AppCacheManifestTransformer extends ResourceTransformerSupport {
6469

70+
private static final Collection<String> MANIFEST_SECTION_HEADERS =
71+
Arrays.asList("CACHE MANIFEST", "NETWORK:", "FALLBACK:", "CACHE:");
72+
6573
private static final String MANIFEST_HEADER = "CACHE MANIFEST";
6674

75+
private static final String CACHE_HEADER = "CACHE:";
76+
6777
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
6878

6979
private static final Log logger = LogFactory.getLog(AppCacheManifestTransformer.class);
7080

7181

72-
private final Map<String, SectionTransformer> sectionTransformers = new HashMap<>();
73-
7482
private final String fileExtension;
7583

7684

@@ -87,144 +95,198 @@ public AppCacheManifestTransformer() {
8795
*/
8896
public AppCacheManifestTransformer(String fileExtension) {
8997
this.fileExtension = fileExtension;
90-
91-
SectionTransformer noOpSection = new NoOpSection();
92-
this.sectionTransformers.put(MANIFEST_HEADER, noOpSection);
93-
this.sectionTransformers.put("NETWORK:", noOpSection);
94-
this.sectionTransformers.put("FALLBACK:", noOpSection);
95-
this.sectionTransformers.put("CACHE:", new CacheSection());
9698
}
9799

98100

99101
@Override
100-
public Resource transform(ServerWebExchange exchange, Resource resource,
101-
ResourceTransformerChain transformerChain) throws IOException {
102+
public Mono<Resource> transform(ServerWebExchange exchange, Resource inputResource,
103+
ResourceTransformerChain chain) {
104+
105+
return chain.transform(exchange, inputResource)
106+
.then(resource -> {
107+
String name = resource.getFilename();
108+
if (!this.fileExtension.equals(StringUtils.getFilenameExtension(name))) {
109+
return Mono.just(resource);
110+
}
111+
String content = new String(getResourceBytes(resource), DEFAULT_CHARSET);
112+
if (!content.startsWith(MANIFEST_HEADER)) {
113+
if (logger.isTraceEnabled()) {
114+
logger.trace("Manifest should start with 'CACHE MANIFEST', skip: " + resource);
115+
}
116+
return Mono.just(resource);
117+
}
118+
if (logger.isTraceEnabled()) {
119+
logger.trace("Transforming resource: " + resource);
120+
}
121+
return Flux.generate(new LineGenerator(content))
122+
.concatMap(info -> processLine(info, exchange, resource, chain))
123+
.collect(() -> new LineAggregator(resource, content), LineAggregator::add)
124+
.then(aggregator -> Mono.just(aggregator.createResource()));
125+
});
126+
}
102127

103-
resource = transformerChain.transform(exchange, resource);
104-
if (!this.fileExtension.equals(StringUtils.getFilenameExtension(resource.getFilename()))) {
105-
return resource;
128+
private static byte[] getResourceBytes(Resource resource) {
129+
try {
130+
return FileCopyUtils.copyToByteArray(resource.getInputStream());
131+
}
132+
catch (IOException ex) {
133+
throw Exceptions.propagate(ex);
106134
}
135+
}
107136

108-
byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
109-
String content = new String(bytes, DEFAULT_CHARSET);
137+
private Mono<LineOutput> processLine(LineInfo info, ServerWebExchange exchange,
138+
Resource resource, ResourceTransformerChain chain) {
110139

111-
if (!content.startsWith(MANIFEST_HEADER)) {
112-
if (logger.isTraceEnabled()) {
113-
logger.trace("AppCache manifest does not start with 'CACHE MANIFEST', skipping: " + resource);
114-
}
115-
return resource;
140+
if (!info.isLink()) {
141+
return Mono.just(new LineOutput(info.getLine(), null));
116142
}
117143

118-
if (logger.isTraceEnabled()) {
119-
logger.trace("Transforming resource: " + resource);
144+
Mono<String> pathMono = resolveUrlPath(info.getLine(), exchange, resource, chain)
145+
.doOnNext(path -> {
146+
if (logger.isTraceEnabled()) {
147+
logger.trace("Link modified: " + path + " (original: " + info.getLine() + ")");
148+
}
149+
});
150+
151+
Mono<Resource> resourceMono = chain.getResolverChain()
152+
.resolveResource(null, info.getLine(), Collections.singletonList(resource));
153+
154+
return Flux.zip(pathMono, resourceMono, LineOutput::new).next();
155+
}
156+
157+
158+
private static class LineGenerator implements Consumer<SynchronousSink<LineInfo>> {
159+
160+
private final Scanner scanner;
161+
162+
private LineInfo previous;
163+
164+
165+
public LineGenerator(String content) {
166+
this.scanner = new Scanner(content);
120167
}
121168

122-
StringWriter contentWriter = new StringWriter();
123-
HashBuilder hashBuilder = new HashBuilder(content.length());
124169

125-
Scanner scanner = new Scanner(content);
126-
SectionTransformer currentTransformer = this.sectionTransformers.get(MANIFEST_HEADER);
127-
while (scanner.hasNextLine()) {
128-
String line = scanner.nextLine();
129-
if (this.sectionTransformers.containsKey(line.trim())) {
130-
currentTransformer = this.sectionTransformers.get(line.trim());
131-
contentWriter.write(line + "\n");
132-
hashBuilder.appendString(line);
170+
@Override
171+
public void accept(SynchronousSink<LineInfo> sink) {
172+
if (this.scanner.hasNext()) {
173+
String line = this.scanner.nextLine();
174+
LineInfo current = new LineInfo(line, this.previous);
175+
sink.next(current);
176+
this.previous = current;
133177
}
134178
else {
135-
contentWriter.write(
136-
currentTransformer.transform(
137-
line, hashBuilder, resource, transformerChain, exchange) + "\n");
179+
sink.complete();
138180
}
139181
}
182+
}
140183

141-
String hash = hashBuilder.build();
142-
contentWriter.write("\n" + "# Hash: " + hash);
143-
if (logger.isTraceEnabled()) {
144-
logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]");
145-
}
184+
private static class LineInfo {
146185

147-
return new TransformedResource(resource, contentWriter.toString().getBytes(DEFAULT_CHARSET));
148-
}
186+
private final String line;
149187

188+
private final boolean cacheSection;
150189

151-
@FunctionalInterface
152-
private interface SectionTransformer {
190+
private final boolean link;
153191

154-
/**
155-
* Transforms a line in a section of the manifest.
156-
* <p>The actual transformation depends on the chosen transformation strategy
157-
* for the current manifest section (CACHE, NETWORK, FALLBACK, etc).
158-
*/
159-
String transform(String line, HashBuilder builder, Resource resource,
160-
ResourceTransformerChain transformerChain, ServerWebExchange exchange) throws IOException;
161-
}
192+
193+
public LineInfo(String line, LineInfo previousLine) {
194+
this.line = line;
195+
this.cacheSection = initCacheSectionFlag(line, previousLine);
196+
this.link = iniLinkFlag(line, this.cacheSection);
197+
}
198+
199+
private static boolean initCacheSectionFlag(String line, LineInfo previousLine) {
200+
if (MANIFEST_SECTION_HEADERS.contains(line.trim())) {
201+
return line.trim().equals(CACHE_HEADER);
202+
}
203+
else if (previousLine != null) {
204+
return previousLine.isCacheSection();
205+
}
206+
throw new IllegalStateException(
207+
"Manifest does not start with " + MANIFEST_HEADER + ": " + line);
208+
}
209+
210+
private static boolean iniLinkFlag(String line, boolean isCacheSection) {
211+
return (isCacheSection && StringUtils.hasText(line) && !line.startsWith("#")
212+
&& !line.startsWith("//") && !hasScheme(line));
213+
}
214+
215+
private static boolean hasScheme(String line) {
216+
int index = line.indexOf(":");
217+
return (line.startsWith("//") || (index > 0 && !line.substring(0, index).contains("/")));
218+
}
162219

163220

164-
private static class NoOpSection implements SectionTransformer {
221+
public String getLine() {
222+
return this.line;
223+
}
165224

166-
public String transform(String line, HashBuilder builder, Resource resource,
167-
ResourceTransformerChain transformerChain, ServerWebExchange exchange) throws IOException {
225+
public boolean isCacheSection() {
226+
return this.cacheSection;
227+
}
168228

169-
builder.appendString(line);
170-
return line;
229+
public boolean isLink() {
230+
return this.link;
171231
}
172232
}
173233

234+
private static class LineOutput {
174235

175-
private class CacheSection implements SectionTransformer {
236+
private final String line;
176237

177-
private static final String COMMENT_DIRECTIVE = "#";
238+
private final Resource resource;
178239

179-
@Override
180-
public String transform(String line, HashBuilder builder, Resource resource,
181-
ResourceTransformerChain transformerChain, ServerWebExchange exchange) throws IOException {
182-
183-
if (isLink(line) && !hasScheme(line)) {
184-
ResourceResolverChain resolverChain = transformerChain.getResolverChain();
185-
Resource appCacheResource =
186-
resolverChain.resolveResource(null, line, Collections.singletonList(resource));
187-
String path = resolveUrlPath(line, exchange, resource, transformerChain);
188-
builder.appendResource(appCacheResource);
189-
if (logger.isTraceEnabled()) {
190-
logger.trace("Link modified: " + path + " (original: " + line + ")");
191-
}
192-
return path;
193-
}
194-
builder.appendString(line);
195-
return line;
240+
241+
public LineOutput(String line, Resource resource) {
242+
this.line = line;
243+
this.resource = resource;
196244
}
197245

198-
private boolean hasScheme(String link) {
199-
int schemeIndex = link.indexOf(":");
200-
return (link.startsWith("//") || (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/")));
246+
public String getLine() {
247+
return this.line;
201248
}
202249

203-
private boolean isLink(String line) {
204-
return (StringUtils.hasText(line) && !line.startsWith(COMMENT_DIRECTIVE));
250+
public Resource getResource() {
251+
return this.resource;
205252
}
206253
}
207254

255+
private static class LineAggregator {
208256

209-
private static class HashBuilder {
257+
private final StringWriter writer = new StringWriter();
210258

211259
private final ByteArrayOutputStream baos;
212260

213-
public HashBuilder(int initialSize) {
214-
this.baos = new ByteArrayOutputStream(initialSize);
215-
}
261+
private final Resource resource;
216262

217-
public void appendResource(Resource resource) throws IOException {
218-
byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream());
219-
this.baos.write(DigestUtils.md5Digest(content));
263+
264+
public LineAggregator(Resource resource, String content) {
265+
this.resource = resource;
266+
this.baos = new ByteArrayOutputStream(content.length());
220267
}
221268

222-
public void appendString(String content) throws IOException {
223-
this.baos.write(content.getBytes(DEFAULT_CHARSET));
269+
public void add(LineOutput lineOutput) {
270+
this.writer.write(lineOutput.getLine() + "\n");
271+
try {
272+
byte[] bytes = (lineOutput.getResource() != null ?
273+
DigestUtils.md5Digest(getResourceBytes(lineOutput.getResource())) :
274+
lineOutput.getLine().getBytes(DEFAULT_CHARSET));
275+
this.baos.write(bytes);
276+
}
277+
catch (IOException ex) {
278+
throw Exceptions.propagate(ex);
279+
}
224280
}
225281

226-
public String build() {
227-
return DigestUtils.md5DigestAsHex(this.baos.toByteArray());
282+
public TransformedResource createResource() {
283+
String hash = DigestUtils.md5DigestAsHex(this.baos.toByteArray());
284+
this.writer.write("\n" + "# Hash: " + hash);
285+
if (logger.isTraceEnabled()) {
286+
logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]");
287+
}
288+
byte[] bytes = this.writer.toString().getBytes(DEFAULT_CHARSET);
289+
return new TransformedResource(this.resource, bytes);
228290
}
229291
}
230292

0 commit comments

Comments
 (0)