18
18
19
19
import java .io .ByteArrayOutputStream ;
20
20
import java .io .IOException ;
21
- import java .io . StringWriter ;
21
+ import java .nio . CharBuffer ;
22
22
import java .nio .charset .Charset ;
23
23
import java .nio .charset .StandardCharsets ;
24
24
import java .util .Arrays ;
25
25
import java .util .Collection ;
26
- import java .util .Collections ;
27
26
import java .util .Scanner ;
28
27
import java .util .function .Consumer ;
29
28
35
34
import reactor .core .publisher .SynchronousSink ;
36
35
37
36
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 ;
38
40
import org .springframework .lang .Nullable ;
39
41
import org .springframework .util .DigestUtils ;
40
- import org .springframework .util .FileCopyUtils ;
42
+ import org .springframework .util .StreamUtils ;
41
43
import org .springframework .util .StringUtils ;
42
44
import org .springframework .web .server .ServerWebExchange ;
43
45
44
46
/**
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.
47
48
*
48
49
* <p>This transformer:
49
50
* <ul>
54
55
* of the manifest in order to trigger an appcache reload in the browser.
55
56
* </ul>
56
57
*
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.
66
63
*
67
64
* @author Rossen Stoyanchev
68
65
* @author Brian Clozel
@@ -107,70 +104,91 @@ public Mono<Resource> transform(ServerWebExchange exchange, Resource inputResour
107
104
ResourceTransformerChain chain ) {
108
105
109
106
return chain .transform (exchange , inputResource )
110
- .flatMap (resource -> {
111
- String name = resource .getFilename ();
107
+ .flatMap (outputResource -> {
108
+ String name = outputResource .getFilename ();
112
109
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 );
121
111
}
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 );
122
145
if (logger .isTraceEnabled ()) {
123
- logger .trace ("Transforming resource: " + resource );
146
+ logger .trace ("AppCache file: [ " + resource . getFilename ()+ "] hash: [" + hash + "]" );
124
147
}
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 ());
129
149
});
130
150
}
131
151
132
- private static byte [] getResourceBytes ( Resource resource ) {
152
+ private static void writeToByteArrayOutputStream ( ByteArrayOutputStream out , String toWrite ) {
133
153
try {
134
- return FileCopyUtils .copyToByteArray (resource .getInputStream ());
154
+ byte [] bytes = toWrite .getBytes (DEFAULT_CHARSET );
155
+ out .write (bytes );
135
156
}
136
157
catch (IOException ex ) {
137
158
throw Exceptions .propagate (ex );
138
159
}
139
160
}
140
161
141
- private Mono <LineOutput > processLine (LineInfo info , ServerWebExchange exchange ,
162
+ private Mono <String > processLine (LineInfo info , ServerWebExchange exchange ,
142
163
Resource resource , ResourceTransformerChain chain ) {
143
164
144
165
if (!info .isLink ()) {
145
- return Mono .just (new LineOutput ( info .getLine (), null ));
166
+ return Mono .just (info .getLine ());
146
167
}
147
168
148
169
String link = toAbsolutePath (info .getLine (), exchange );
149
- Mono < String > pathMono = resolveUrlPath (link , exchange , resource , chain )
170
+ return resolveUrlPath (link , exchange , resource , chain )
150
171
.doOnNext (path -> {
151
172
if (logger .isTraceEnabled ()) {
152
173
logger .trace ("Link modified: " + path + " (original: " + info .getLine () + ")" );
153
174
}
154
175
});
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 ();
160
176
}
161
177
162
178
163
- private static class LineGenerator implements Consumer <SynchronousSink <LineInfo >> {
179
+ private static class LineInfoGenerator implements Consumer <SynchronousSink <LineInfo >> {
164
180
165
181
private final Scanner scanner ;
166
182
167
183
@ Nullable
168
184
private LineInfo previous ;
169
185
170
- public LineGenerator (String content ) {
186
+
187
+ LineInfoGenerator (String content ) {
171
188
this .scanner = new Scanner (content );
172
189
}
173
190
191
+
174
192
@ Override
175
193
public void accept (SynchronousSink <LineInfo > sink ) {
176
194
if (this .scanner .hasNext ()) {
@@ -194,12 +212,14 @@ private static class LineInfo {
194
212
195
213
private final boolean link ;
196
214
197
- public LineInfo (String line , @ Nullable LineInfo previousLine ) {
215
+
216
+ LineInfo (String line , @ Nullable LineInfo previousLine ) {
198
217
this .line = line ;
199
218
this .cacheSection = initCacheSectionFlag (line , previousLine );
200
219
this .link = iniLinkFlag (line , this .cacheSection );
201
220
}
202
221
222
+
203
223
private static boolean initCacheSectionFlag (String line , @ Nullable LineInfo previousLine ) {
204
224
if (MANIFEST_SECTION_HEADERS .contains (line .trim ())) {
205
225
return line .trim ().equals (CACHE_HEADER );
@@ -221,6 +241,7 @@ private static boolean hasScheme(String line) {
221
241
return (line .startsWith ("//" ) || (index > 0 && !line .substring (0 , index ).contains ("/" )));
222
242
}
223
243
244
+
224
245
public String getLine () {
225
246
return this .line ;
226
247
}
@@ -234,65 +255,4 @@ public boolean isLink() {
234
255
}
235
256
}
236
257
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
-
298
258
}
0 commit comments