1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.springframework.web.servlet.resource;
18
19 import java.io.ByteArrayOutputStream;
20 import java.io.IOException;
21 import java.io.StringWriter;
22 import java.nio.charset.Charset;
23 import java.util.Arrays;
24 import java.util.HashMap;
25 import java.util.Map;
26 import java.util.Scanner;
27 import javax.servlet.http.HttpServletRequest;
28
29 import org.apache.commons.logging.Log;
30 import org.apache.commons.logging.LogFactory;
31
32 import org.springframework.core.io.Resource;
33 import org.springframework.util.DigestUtils;
34 import org.springframework.util.FileCopyUtils;
35 import org.springframework.util.StringUtils;
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59 public class AppCacheManifestTransformer extends ResourceTransformerSupport {
60
61 private static final String MANIFEST_HEADER = "CACHE MANIFEST";
62
63 private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
64
65 private static final Log logger = LogFactory.getLog(AppCacheManifestTransformer.class);
66
67
68 private final Map<String, SectionTransformer> sectionTransformers = new HashMap<String, SectionTransformer>();
69
70 private final String fileExtension;
71
72
73
74
75
76 public AppCacheManifestTransformer() {
77 this("manifest");
78 }
79
80
81
82
83
84 public AppCacheManifestTransformer(String fileExtension) {
85 this.fileExtension = fileExtension;
86
87 SectionTransformer noOpSection = new NoOpSection();
88 this.sectionTransformers.put(MANIFEST_HEADER, noOpSection);
89 this.sectionTransformers.put("NETWORK:", noOpSection);
90 this.sectionTransformers.put("FALLBACK:", noOpSection);
91 this.sectionTransformers.put("CACHE:", new CacheSection());
92 }
93
94
95 @Override
96 public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain)
97 throws IOException {
98
99 resource = transformerChain.transform(request, resource);
100
101 String filename = resource.getFilename();
102 if (!this.fileExtension.equals(StringUtils.getFilenameExtension(filename))) {
103 return resource;
104 }
105
106 byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
107 String content = new String(bytes, DEFAULT_CHARSET);
108
109 if (!content.startsWith(MANIFEST_HEADER)) {
110 if (logger.isTraceEnabled()) {
111 logger.trace("AppCache manifest does not start with 'CACHE MANIFEST', skipping: " + resource);
112 }
113 return resource;
114 }
115
116 if (logger.isTraceEnabled()) {
117 logger.trace("Transforming resource: " + resource);
118 }
119
120 StringWriter contentWriter = new StringWriter();
121 HashBuilder hashBuilder = new HashBuilder(content.length());
122
123 Scanner scanner = new Scanner(content);
124 SectionTransformer currentTransformer = this.sectionTransformers.get(MANIFEST_HEADER);
125 while (scanner.hasNextLine()) {
126 String line = scanner.nextLine();
127 if (this.sectionTransformers.containsKey(line.trim())) {
128 currentTransformer = this.sectionTransformers.get(line.trim());
129 contentWriter.write(line + "\n");
130 hashBuilder.appendString(line);
131 }
132 else {
133 contentWriter.write(
134 currentTransformer.transform(line, hashBuilder, resource, transformerChain, request) + "\n");
135 }
136 }
137
138 String hash = hashBuilder.build();
139 contentWriter.write("\n" + "# Hash: " + hash);
140 if (logger.isTraceEnabled()) {
141 logger.trace("AppCache file: [" + resource.getFilename()+ "] Hash: [" + hash + "]");
142 }
143
144 return new TransformedResource(resource, contentWriter.toString().getBytes(DEFAULT_CHARSET));
145 }
146
147
148 private static interface SectionTransformer {
149
150
151
152
153
154
155 String transform(String line, HashBuilder builder, Resource resource,
156 ResourceTransformerChain transformerChain, HttpServletRequest request) throws IOException;
157 }
158
159
160 private static class NoOpSection implements SectionTransformer {
161
162 public String transform(String line, HashBuilder builder, Resource resource,
163 ResourceTransformerChain transformerChain, HttpServletRequest request) throws IOException {
164
165 builder.appendString(line);
166 return line;
167 }
168 }
169
170
171 private class CacheSection implements SectionTransformer {
172
173 private final String COMMENT_DIRECTIVE = "#";
174
175 @Override
176 public String transform(String line, HashBuilder builder, Resource resource,
177 ResourceTransformerChain transformerChain, HttpServletRequest request) throws IOException {
178
179 if (isLink(line) && !hasScheme(line)) {
180 ResourceResolverChain resolverChain = transformerChain.getResolverChain();
181 Resource appCacheResource = resolverChain.resolveResource(null, line, Arrays.asList(resource));
182 String path = resolveUrlPath(line, request, resource, transformerChain);
183 builder.appendResource(appCacheResource);
184 if (logger.isTraceEnabled()) {
185 logger.trace("Link modified: " + path + " (original: " + line + ")");
186 }
187 return path;
188 }
189 builder.appendString(line);
190 return line;
191 }
192
193 private boolean hasScheme(String link) {
194 int schemeIndex = link.indexOf(":");
195 return (link.startsWith("//") || (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/")));
196 }
197
198 private boolean isLink(String line) {
199 return (StringUtils.hasText(line) && !line.startsWith(COMMENT_DIRECTIVE));
200 }
201 }
202
203
204 private static class HashBuilder {
205
206 private final ByteArrayOutputStream baos;
207
208 public HashBuilder(int initialSize) {
209 this.baos = new ByteArrayOutputStream(initialSize);
210 }
211
212 public void appendResource(Resource resource) throws IOException {
213 byte[] content = FileCopyUtils.copyToByteArray(resource.getInputStream());
214 this.baos.write(DigestUtils.md5Digest(content));
215 }
216
217 public void appendString(String content) throws IOException {
218 this.baos.write(content.getBytes(DEFAULT_CHARSET));
219 }
220
221 public String build() {
222 return DigestUtils.md5DigestAsHex(this.baos.toByteArray());
223 }
224 }
225
226 }