2121import java .io .StringWriter ;
2222import java .nio .charset .Charset ;
2323import java .nio .charset .StandardCharsets ;
24+ import java .util .Arrays ;
25+ import java .util .Collection ;
2426import java .util .Collections ;
25- import java .util .HashMap ;
26- import java .util .Map ;
2727import java .util .Scanner ;
28+ import java .util .function .Consumer ;
2829
2930import org .apache .commons .logging .Log ;
3031import 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
3237import org .springframework .core .io .Resource ;
3338import org .springframework .util .DigestUtils ;
6267 */
6368public 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