All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.web.reactive.resource.AppCacheManifestTransformer Maven / Gradle / Ivy

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.reactive.resource;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
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.Scanner;
import java.util.function.Consumer;

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 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.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;

/**
 * A {@link ResourceTransformer} HTML5 AppCache manifests.
 *
 * 

This transformer: *

    *
  • modifies links to match the public URL paths that should be exposed to * clients, using configured {@code ResourceResolver} strategies *
  • appends a comment in the manifest, containing a Hash * (e.g. "# Hash: 9de0f09ed7caf84e885f1f0f11c7e326"), thus changing the content * of the manifest in order to trigger an appcache reload in the browser. *
* *

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 * @since 5.0 * @see HTML5 offline applications spec */ public class AppCacheManifestTransformer extends ResourceTransformerSupport { private static final Collection MANIFEST_SECTION_HEADERS = Arrays.asList("CACHE MANIFEST", "NETWORK:", "FALLBACK:", "CACHE:"); private static final String MANIFEST_HEADER = "CACHE MANIFEST"; private static final String CACHE_HEADER = "CACHE:"; private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private static final Log logger = LogFactory.getLog(AppCacheManifestTransformer.class); private final String fileExtension; /** * Create an AppCacheResourceTransformer that transforms files with extension ".appcache". */ public AppCacheManifestTransformer() { this("appcache"); } /** * Create an AppCacheResourceTransformer that transforms files with the extension * given as a parameter. */ public AppCacheManifestTransformer(String fileExtension) { this.fileExtension = fileExtension; } @Override public Mono transform(ServerWebExchange exchange, Resource inputResource, ResourceTransformerChain chain) { return chain.transform(exchange, inputResource) .flatMap(outputResource -> { String name = outputResource.getFilename(); if (!this.fileExtension.equals(StringUtils.getFilenameExtension(name))) { return Mono.just(outputResource); } DataBufferFactory bufferFactory = exchange.getResponse().bufferFactory(); Flux flux = DataBufferUtils .read(outputResource, bufferFactory, StreamUtils.BUFFER_SIZE); return DataBufferUtils.join(flux) .flatMap(dataBuffer -> { CharBuffer charBuffer = DEFAULT_CHARSET.decode(dataBuffer.asByteBuffer()); DataBufferUtils.release(dataBuffer); String content = charBuffer.toString(); return transform(content, outputResource, chain, exchange); }); }); } private Mono 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("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]"); } return new TransformedResource(resource, out.toByteArray()); }); } private static void writeToByteArrayOutputStream(ByteArrayOutputStream out, String toWrite) { try { byte[] bytes = toWrite.getBytes(DEFAULT_CHARSET); out.write(bytes); } catch (IOException ex) { throw Exceptions.propagate(ex); } } private Mono processLine(LineInfo info, ServerWebExchange exchange, Resource resource, ResourceTransformerChain chain) { if (!info.isLink()) { return Mono.just(info.getLine()); } String link = toAbsolutePath(info.getLine(), exchange); return resolveUrlPath(link, exchange, resource, chain) .doOnNext(path -> { if (logger.isTraceEnabled()) { logger.trace("Link modified: " + path + " (original: " + info.getLine() + ")"); } }); } private static class LineInfoGenerator implements Consumer> { private final Scanner scanner; @Nullable private LineInfo previous; LineInfoGenerator(String content) { this.scanner = new Scanner(content); } @Override public void accept(SynchronousSink sink) { if (this.scanner.hasNext()) { String line = this.scanner.nextLine(); LineInfo current = new LineInfo(line, this.previous); sink.next(current); this.previous = current; } else { sink.complete(); } } } private static class LineInfo { private final String line; private final boolean cacheSection; private final boolean link; 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); } else if (previousLine != null) { return previousLine.isCacheSection(); } throw new IllegalStateException( "Manifest does not start with " + MANIFEST_HEADER + ": " + line); } private static boolean iniLinkFlag(String line, boolean isCacheSection) { return (isCacheSection && StringUtils.hasText(line) && !line.startsWith("#") && !line.startsWith("//") && !hasScheme(line)); } private static boolean hasScheme(String line) { int index = line.indexOf(':'); return (line.startsWith("//") || (index > 0 && !line.substring(0, index).contains("/"))); } public String getLine() { return this.line; } public boolean isCacheSection() { return this.cacheSection; } public boolean isLink() { return this.link; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy