diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AppCacheManifestTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AppCacheManifestTransformer.java index f4e3a486227..4e23f9adb1a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AppCacheManifestTransformer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/AppCacheManifestTransformer.java @@ -21,9 +21,9 @@ import java.io.IOException; import java.io.StringWriter; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; import java.util.Scanner; import javax.servlet.http.HttpServletRequest; @@ -59,15 +59,18 @@ import org.springframework.util.StringUtils; */ 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 Map sectionTransformers = new HashMap<>(); - private final String fileExtension; @@ -84,20 +87,14 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport { */ public AppCacheManifestTransformer(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 { + public Resource transform(HttpServletRequest request, Resource resource, + ResourceTransformerChain chain) throws IOException { - resource = transformerChain.transform(request, resource); + resource = chain.transform(request, resource); if (!this.fileExtension.equals(StringUtils.getFilenameExtension(resource.getFilename()))) { return resource; } @@ -107,7 +104,7 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport { if (!content.startsWith(MANIFEST_HEADER)) { if (logger.isTraceEnabled()) { - logger.trace("AppCache manifest does not start with 'CACHE MANIFEST', skipping: " + resource); + logger.trace("Manifest should start with 'CACHE MANIFEST', skip: " + resource); } return resource; } @@ -116,111 +113,145 @@ public class AppCacheManifestTransformer extends ResourceTransformerSupport { 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()) { + LineInfo previous = null; + LineAggregator aggregator = new LineAggregator(resource, content); + + while (scanner.hasNext()) { 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, request) + "\n"); - } + LineInfo current = new LineInfo(line, previous); + LineOutput lineOutput = processLine(current, request, resource, chain); + aggregator.add(lineOutput); + previous = current; } - String hash = hashBuilder.build(); - contentWriter.write("\n" + "# Hash: " + hash); + return aggregator.createResource(); + } + + private static byte[] getResourceBytes(Resource resource) throws IOException { + return FileCopyUtils.copyToByteArray(resource.getInputStream()); + } + + private LineOutput processLine(LineInfo info, HttpServletRequest request, + Resource resource, ResourceTransformerChain transformerChain) { + + if (!info.isLink()) { + return new LineOutput(info.getLine(), null); + } + + Resource appCacheResource = transformerChain.getResolverChain() + .resolveResource(null, info.getLine(), Collections.singletonList(resource)); + + String path = resolveUrlPath(info.getLine(), request, resource, transformerChain); if (logger.isTraceEnabled()) { - logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]"); + logger.trace("Link modified: " + path + " (original: " + info.getLine() + ")"); } - return new TransformedResource(resource, contentWriter.toString().getBytes(DEFAULT_CHARSET)); + return new LineOutput(path, appCacheResource); } - @FunctionalInterface - private interface SectionTransformer { + private static class LineInfo { - /** - * Transforms a line in a section of the manifest. - *

The actual transformation depends on the chosen transformation strategy - * for the current manifest section (CACHE, NETWORK, FALLBACK, etc). - */ - String transform(String line, HashBuilder builder, Resource resource, - ResourceTransformerChain transformerChain, HttpServletRequest request) throws IOException; - } + private final String line; + + private final boolean cacheSection; + + private final boolean link; - private static class NoOpSection implements SectionTransformer { - - public String transform(String line, HashBuilder builder, Resource resource, - ResourceTransformerChain transformerChain, HttpServletRequest request) throws IOException { - - builder.appendString(line); - return line; + public LineInfo(String line, LineInfo previous) { + this.line = line; + this.cacheSection = initCacheSectionFlag(line, previous); + this.link = iniLinkFlag(line, this.cacheSection); } - } - - private class CacheSection implements SectionTransformer { - - private static final String COMMENT_DIRECTIVE = "#"; - - @Override - public String transform(String line, HashBuilder builder, Resource resource, - ResourceTransformerChain transformerChain, HttpServletRequest request) throws IOException { - - if (isLink(line) && !hasScheme(line)) { - ResourceResolverChain resolverChain = transformerChain.getResolverChain(); - Resource appCacheResource = - resolverChain.resolveResource(null, line, Collections.singletonList(resource)); - String path = resolveUrlPath(line, request, resource, transformerChain); - builder.appendResource(appCacheResource); - if (logger.isTraceEnabled()) { - logger.trace("Link modified: " + path + " (original: " + line + ")"); - } - return path; + private static boolean initCacheSectionFlag(String line, LineInfo previousLine) { + if (MANIFEST_SECTION_HEADERS.contains(line.trim())) { + return line.trim().equals(CACHE_HEADER); } - builder.appendString(line); - return line; + else if (previousLine != null) { + return previousLine.isCacheSection(); + } + throw new IllegalStateException( + "Manifest does not start with " + MANIFEST_HEADER + ": " + line); } - private boolean hasScheme(String link) { - int schemeIndex = link.indexOf(":"); - return (link.startsWith("//") || (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/"))); + private static boolean iniLinkFlag(String line, boolean isCacheSection) { + return (isCacheSection && StringUtils.hasText(line) && !line.startsWith("#") + && !line.startsWith("//") && !hasScheme(line)); } - private boolean isLink(String line) { - return (StringUtils.hasText(line) && !line.startsWith(COMMENT_DIRECTIVE)); + 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; } } + private static class LineOutput { - private static class HashBuilder { + private final String line; + + private final Resource resource; + + + public LineOutput(String line, Resource resource) { + this.line = line; + this.resource = resource; + } + + public String getLine() { + return this.line; + } + + public Resource getResource() { + return this.resource; + } + } + + private static class LineAggregator { + + private final StringWriter writer = new StringWriter(); private final ByteArrayOutputStream baos; - public HashBuilder(int initialSize) { - this.baos = new ByteArrayOutputStream(initialSize); + private final Resource resource; + + + public LineAggregator(Resource resource, String content) { + this.resource = resource; + this.baos = new ByteArrayOutputStream(content.length()); } - public void appendResource(Resource resource) throws IOException { - byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream()); - this.baos.write(DigestUtils.md5Digest(content)); + public void add(LineOutput lineOutput) throws IOException { + this.writer.write(lineOutput.getLine() + "\n"); + byte[] bytes = (lineOutput.getResource() != null ? + DigestUtils.md5Digest(getResourceBytes(lineOutput.getResource())) : + lineOutput.getLine().getBytes(DEFAULT_CHARSET)); + this.baos.write(bytes); } - public void appendString(String content) throws IOException { - this.baos.write(content.getBytes(DEFAULT_CHARSET)); - } - - public String build() { - return DigestUtils.md5DigestAsHex(this.baos.toByteArray()); + public TransformedResource createResource() { + String hash = DigestUtils.md5DigestAsHex(this.baos.toByteArray()); + this.writer.write("\n" + "# Hash: " + hash); + if (logger.isTraceEnabled()) { + logger.trace("AppCache file: [" + resource.getFilename()+ "] hash: [" + hash + "]"); + } + byte[] bytes = this.writer.toString().getBytes(DEFAULT_CHARSET); + return new TransformedResource(this.resource, bytes); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java index 86f43d6ffa2..9c002430edf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java @@ -54,12 +54,12 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { private static final Log logger = LogFactory.getLog(CssLinkResourceTransformer.class); - private final List linkParsers = new ArrayList<>(2); + private final List linkParsers = new ArrayList<>(2); public CssLinkResourceTransformer() { - this.linkParsers.add(new ImportStatementCssLinkParser()); - this.linkParsers.add(new UrlFunctionCssLinkParser()); + this.linkParsers.add(new ImportStatementLinkParser()); + this.linkParsers.add(new UrlFunctionLinkParser()); } @@ -81,26 +81,25 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream()); String content = new String(bytes, DEFAULT_CHARSET); - Set infos = new HashSet<>(8); - for (CssLinkParser parser : this.linkParsers) { - parser.parseLink(content, infos); + List linkSegments = new ArrayList<>(8); + for (LinkParser parser : this.linkParsers) { + linkSegments.addAll(parser.parseLink(content)); } - if (infos.isEmpty()) { + if (linkSegments.isEmpty()) { if (logger.isTraceEnabled()) { logger.trace("No links found."); } return resource; } - List sortedInfos = new ArrayList<>(infos); - Collections.sort(sortedInfos); + Collections.sort(linkSegments); int index = 0; StringWriter writer = new StringWriter(); - for (CssLinkInfo info : sortedInfos) { - writer.write(content.substring(index, info.getStart())); - String link = content.substring(info.getStart(), info.getEnd()); + for (Segment linkSegment : linkSegments) { + writer.write(content.substring(index, linkSegment.getStart())); + String link = content.substring(linkSegment.getStart(), linkSegment.getEnd()); String newLink = null; if (!hasScheme(link)) { newLink = resolveUrlPath(link, request, resource, transformerChain); @@ -114,7 +113,7 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { } } writer.write(newLink != null ? newLink : link); - index = info.getEnd(); + index = linkSegment.getEnd(); } writer.write(content.substring(index)); @@ -128,13 +127,14 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { @FunctionalInterface - protected interface CssLinkParser { + protected interface LinkParser { + + Set parseLink(String content); - void parseLink(String content, Set linkInfos); } - protected static abstract class AbstractCssLinkParser implements CssLinkParser { + protected static abstract class AbstractLinkParser implements LinkParser { /** * Return the keyword to use to search for links. @@ -142,7 +142,8 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { protected abstract String getKeyword(); @Override - public void parseLink(String content, Set linkInfos) { + public Set parseLink(String content) { + Set linksToAdd = new HashSet<>(8); int index = 0; do { index = content.indexOf(getKeyword(), index); @@ -151,17 +152,18 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { } index = skipWhitespace(content, index + getKeyword().length()); if (content.charAt(index) == '\'') { - index = addLink(index, "'", content, linkInfos); + index = addLink(index, "'", content, linksToAdd); } else if (content.charAt(index) == '"') { - index = addLink(index, "\"", content, linkInfos); + index = addLink(index, "\"", content, linksToAdd); } else { - index = extractLink(index, content, linkInfos); + index = extractLink(index, content, linksToAdd); } } while (true); + return linksToAdd; } private int skipWhitespace(String content, int index) { @@ -174,10 +176,10 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { } } - protected int addLink(int index, String endKey, String content, Set linkInfos) { + protected int addLink(int index, String endKey, String content, Set linksToAdd) { int start = index + 1; int end = content.indexOf(endKey, start); - linkInfos.add(new CssLinkInfo(start, end)); + linksToAdd.add(new Segment(start, end)); return end + endKey.length(); } @@ -185,12 +187,12 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { * Invoked after a keyword match, after whitespaces removed, and when * the next char is neither a single nor double quote. */ - protected abstract int extractLink(int index, String content, Set linkInfos); + protected abstract int extractLink(int index, String content, Set linksToAdd); } - private static class ImportStatementCssLinkParser extends AbstractCssLinkParser { + private static class ImportStatementLinkParser extends AbstractLinkParser { @Override protected String getKeyword() { @@ -198,7 +200,7 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { } @Override - protected int extractLink(int index, String content, Set linkInfos) { + protected int extractLink(int index, String content, Set linksToAdd) { if (content.substring(index, index + 4).equals("url(")) { // Ignore, UrlLinkParser will take care } @@ -210,7 +212,7 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { } - private static class UrlFunctionCssLinkParser extends AbstractCssLinkParser { + private static class UrlFunctionLinkParser extends AbstractLinkParser { @Override protected String getKeyword() { @@ -218,20 +220,20 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { } @Override - protected int extractLink(int index, String content, Set linkInfos) { + protected int extractLink(int index, String content, Set linksToAdd) { // A url() function without unquoted - return addLink(index - 1, ")", content, linkInfos); + return addLink(index - 1, ")", content, linksToAdd); } } - private static class CssLinkInfo implements Comparable { + private static class Segment implements Comparable { private final int start; private final int end; - public CssLinkInfo(int start, int end) { + public Segment(int start, int end) { this.start = start; this.end = end; } @@ -245,7 +247,7 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { } @Override - public int compareTo(CssLinkInfo other) { + public int compareTo(Segment other) { return (this.start < other.start ? -1 : (this.start == other.start ? 0 : 1)); } @@ -254,8 +256,8 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { if (this == obj) { return true; } - if (obj != null && obj instanceof CssLinkInfo) { - CssLinkInfo other = (CssLinkInfo) obj; + if (obj != null && obj instanceof Segment) { + Segment other = (Segment) obj; return (this.start == other.start && this.end == other.end); } return false; @@ -267,4 +269,4 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { } } -} +} \ No newline at end of file