org.springframework.web.servlet.resource.AppCacheManifestTransfomer.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.web.servlet.resource.AppCacheManifestTransfomer.java

Source

/*
 * Copyright 2002-2014 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.servlet.resource;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.core.io.Resource;
import org.springframework.util.DigestUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.StringUtils;

/**
 * A {@link ResourceTransformer} implementation that helps handling resources
 * within HTML5 AppCache manifests for HTML5 offline applications.
 *
 * <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>
 *
 * All files that have the ".manifest" file extension, or the extension given in the constructor, will be transformed
 * by this class.
 *
 * This hash is computed using the content of the appcache manifest and the content of the linked resources; so
 * changing a resource linked in the manifest or the manifest itself should invalidate browser cache.
 *
 * @author Brian Clozel
 * @see <a href="http://www.whatwg.org/specs/web-apps/current-work/multipage/offline.html#offline">HTML5 offline
 * applications spec</a>
 * @since 4.1
 */
public class AppCacheManifestTransfomer implements ResourceTransformer {

    private static final String MANIFEST_HEADER = "CACHE MANIFEST";

    private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    private static final Log logger = LogFactory.getLog(AppCacheManifestTransfomer.class);

    private final Map<String, SectionTransformer> sectionTransformers = new HashMap<String, SectionTransformer>();

    private final String fileExtension;

    /**
     * Create an AppCacheResourceTransformer that transforms files with extension ".manifest"
     */
    public AppCacheManifestTransfomer() {
        this("manifest");
    }

    /**
     * Create an AppCacheResourceTransformer that transforms files with the extension
     * given as a parameter.
     */
    public AppCacheManifestTransfomer(String fileExtension) {
        this.fileExtension = fileExtension;

        SectionTransformer noOpSection = new NoOpSection();
        this.sectionTransformers.put(MANIFEST_HEADER, noOpSection);
        this.sectionTransformers.put("NETWORK:", noOpSection);
        this.sectionTransformers.put("FALLBACK:", noOpSection);
        this.sectionTransformers.put("CACHE:", new CacheSection());
    }

    @Override
    public Resource transform(HttpServletRequest request, Resource resource,
            ResourceTransformerChain transformerChain) throws IOException {
        resource = transformerChain.transform(request, resource);

        String filename = resource.getFilename();
        if (!this.fileExtension.equals(StringUtils.getFilenameExtension(filename))) {
            return resource;
        }

        byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
        String content = new String(bytes, DEFAULT_CHARSET);

        if (!content.startsWith(MANIFEST_HEADER)) {
            if (logger.isTraceEnabled()) {
                logger.trace("AppCache manifest does not start with 'CACHE MANIFEST', skipping: " + resource);
            }
            return resource;
        }

        if (logger.isTraceEnabled()) {
            logger.trace("Transforming resource: " + resource);
        }

        StringWriter contentWriter = new StringWriter();
        HashBuilder hashBuilder = new HashBuilder(content.length());

        Scanner scanner = new Scanner(content);
        SectionTransformer currentTransformer = this.sectionTransformers.get(MANIFEST_HEADER);
        while (scanner.hasNextLine()) {
            String line = scanner.nextLine();
            if (this.sectionTransformers.containsKey(line.trim())) {
                currentTransformer = this.sectionTransformers.get(line.trim());
                contentWriter.write(line + "\n");
                hashBuilder.appendString(line);
            } else {
                contentWriter
                        .write(currentTransformer.transform(line, hashBuilder, resource, transformerChain) + "\n");
            }
        }

        String hash = hashBuilder.build();
        contentWriter.write("\n" + "# Hash: " + hash);
        if (logger.isTraceEnabled()) {
            logger.trace("AppCache file: [" + resource.getFilename() + "] Hash: [" + hash + "]");
        }

        return new TransformedResource(resource, contentWriter.toString().getBytes(DEFAULT_CHARSET));
    }

    private static interface SectionTransformer {

        /**
         * Transforms a line in a section of the manifest
         * <p>The actual transformation depends on the chose transformation strategy
         * for the current manifest section (CACHE, NETWORK, FALLBACK, etc).
         */
        String transform(String line, HashBuilder builder, Resource resource,
                ResourceTransformerChain transformerChain) throws IOException;
    }

    private static class NoOpSection implements SectionTransformer {

        public String transform(String line, HashBuilder builder, Resource resource,
                ResourceTransformerChain transformerChain) throws IOException {

            builder.appendString(line);
            return line;
        }
    }

    private static class CacheSection implements SectionTransformer {

        private final String COMMENT_DIRECTIVE = "#";

        @Override
        public String transform(String line, HashBuilder builder, Resource resource,
                ResourceTransformerChain transformerChain) throws IOException {

            if (isLink(line) && !hasScheme(line)) {
                Resource appCacheResource = transformerChain.getResolverChain().resolveResource(null, line,
                        Arrays.asList(resource));
                String path = transformerChain.getResolverChain().resolveUrlPath(line, Arrays.asList(resource));
                builder.appendResource(appCacheResource);
                if (logger.isTraceEnabled()) {
                    logger.trace("Link modified: " + path + " (original: " + line + ")");
                }
                return path;
            }
            builder.appendString(line);
            return line;
        }

        private boolean hasScheme(String link) {
            int schemeIndex = link.indexOf(":");
            return (link.startsWith("//") || (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/")));
        }

        private boolean isLink(String line) {
            return (StringUtils.hasText(line) && !line.startsWith(COMMENT_DIRECTIVE));
        }
    }

    private static class HashBuilder {

        private final ByteArrayOutputStream baos;

        public HashBuilder(int initialSize) {
            this.baos = new ByteArrayOutputStream(initialSize);
        }

        public void appendResource(Resource resource) throws IOException {
            byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream());
            this.baos.write(DigestUtils.md5Digest(content));
        }

        public void appendString(String content) throws IOException {
            this.baos.write(content.getBytes(DEFAULT_CHARSET));
        }

        public String build() {
            return DigestUtils.md5DigestAsHex(this.baos.toByteArray());
        }
    }

}