Java tutorial
/* * 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()); } } }