org.springframework.web.reactive.resource.AppCacheManifestTransformer.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.web.reactive.resource.AppCacheManifestTransformer.java

Source

/*
 * 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.
 *
 * <p>This transformer:
 * <ul>
 * <li>modifies links to match the public URL paths that should be exposed to
 * clients, using configured {@code ResourceResolver} strategies
 * <li>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.
 * </ul>
 *
 * <p>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 <a href="https://html.spec.whatwg.org/multipage/browsers.html#offline">HTML5 offline applications spec</a>
 */
public class AppCacheManifestTransformer extends ResourceTransformerSupport {

    private static final Collection<String> 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<Resource> 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<DataBuffer> 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<? extends Resource> 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<String> 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<SynchronousSink<LineInfo>> {

        private final Scanner scanner;

        @Nullable
        private LineInfo previous;

        LineInfoGenerator(String content) {
            this.scanner = new Scanner(content);
        }

        @Override
        public void accept(SynchronousSink<LineInfo> 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;
        }
    }

}