1 | // Copyright (C) 2009 Google Inc. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | package com.google.caja.ancillary.servlet; |
16 | |
17 | import com.google.caja.lexer.CharProducer; |
18 | import com.google.caja.lexer.GuessContentType; |
19 | import com.google.caja.lexer.InputSource; |
20 | import com.google.caja.lexer.ParseException; |
21 | import com.google.caja.reporting.DevNullMessageQueue; |
22 | import com.google.caja.util.Charsets; |
23 | import com.google.caja.util.ContentType; |
24 | import com.google.caja.util.Lists; |
25 | |
26 | import java.io.ByteArrayOutputStream; |
27 | import java.io.IOException; |
28 | import java.io.InputStream; |
29 | import java.io.InputStreamReader; |
30 | import java.io.OutputStream; |
31 | import java.io.Writer; |
32 | import java.net.URISyntaxException; |
33 | import java.util.Collections; |
34 | import java.util.LinkedHashMap; |
35 | import java.util.List; |
36 | import java.util.Map; |
37 | import javax.servlet.http.HttpServletRequest; |
38 | import javax.servlet.http.HttpServletResponse; |
39 | |
40 | /** |
41 | * Serves static files from the class path. |
42 | * Files are stored in the <tt>files</tt> directory in this package. There |
43 | * are several kinds of file:<ul> |
44 | * <li>Images, scripts, and help files linked to from other pages. |
45 | * <li>Help files based on identifiers in the java code. E.g. the help text |
46 | * for a warning message {@code MessageType.FOO_BAR} would be in the html |
47 | * file <tt>files/FOO_BAR_tip.html</tt>. |
48 | * </ul> |
49 | * |
50 | * @author mikesamuel@gmail.com |
51 | */ |
52 | final class StaticFiles { |
53 | // Use a bigger cache to store the fact that a file does not exist, since |
54 | // we don't have to use much memory for it. |
55 | private final Map<String, Boolean> exists = mruCache(1024); |
56 | private final Map<String, Content> files = mruCache(128); |
57 | private final long startupTime = System.currentTimeMillis(); |
58 | private final long expiryDate = Math.max( |
59 | startupTime + 3600 * 24 * 366000, (0x7fffffffL * 1000)); |
60 | final String cacheId; |
61 | |
62 | StaticFiles(String cacheId) { |
63 | this.cacheId = cacheId; |
64 | } |
65 | |
66 | boolean exists(String path) { |
67 | if (files.containsKey(path)) { return true; } |
68 | Boolean b = exists.get(path); |
69 | if (b != null) { return b; } |
70 | InputStream in = StaticFiles.class.getResourceAsStream(path); |
71 | boolean fileExists; |
72 | if (in != null) { |
73 | try { |
74 | in.close(); |
75 | } catch (IOException ex) { |
76 | ex.printStackTrace(); |
77 | } |
78 | fileExists = true; |
79 | } else { |
80 | fileExists = false; |
81 | } |
82 | exists.put(path, fileExists); |
83 | return fileExists; |
84 | } |
85 | |
86 | void serve(String path, HttpServletRequest req, HttpServletResponse resp) |
87 | throws IOException { |
88 | if (req.getDateHeader("If-modified-since") >= startupTime) { |
89 | resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); |
90 | return; |
91 | } |
92 | Content content = files.get(path); |
93 | if (content == null && !Boolean.FALSE.equals(exists.get(path))) { |
94 | InputStream in = StaticFiles.class.getResourceAsStream(path); |
95 | if (in == null) { |
96 | exists.put(path, Boolean.FALSE); |
97 | } else { |
98 | try { |
99 | // TODO(mikesamuel): SVN has it in svn:mime-type, but that is not |
100 | // available via the ClassLoader. Is there any way to get at it? |
101 | ContentType t = GuessContentType.guess(null, path, null); |
102 | if (t != null && t.isText) { |
103 | InputSource is; |
104 | try { |
105 | is = new InputSource(StaticFiles.class.getResource(path).toURI()); |
106 | } catch (URISyntaxException ex) { |
107 | ex.printStackTrace(); |
108 | is = InputSource.UNKNOWN; |
109 | } |
110 | CharProducer cp = CharProducer.Factory.create( |
111 | new InputStreamReader(in, Charsets.UTF_8), is); |
112 | // Minimize it before serving. |
113 | Request min = Request.create(Verb.ECHO, this); |
114 | min.minify = true; |
115 | min.opt = true; |
116 | min.otype = t; |
117 | Processor p = new Processor(min, DevNullMessageQueue.singleton()); |
118 | try { |
119 | Job j = p.parse(cp.clone(), t, null, is.getUri()); |
120 | List<Job> out = p.process(Lists.newArrayList(j)); |
121 | if (out.size() == 1) { |
122 | content = p.reduce(out); |
123 | } |
124 | } catch (ParseException ex) { |
125 | // fall through to case below |
126 | ex.printStackTrace(); |
127 | } |
128 | if (content == null) { |
129 | content = new Content(cp.toString(), t); |
130 | } |
131 | } else { |
132 | ByteArrayOutputStream buf = new ByteArrayOutputStream(); |
133 | byte[] bytes = new byte[4096]; |
134 | for (int n; (n = in.read(bytes)) > 0;) { buf.write(bytes, 0, n); } |
135 | content = new Content(bytes, t); |
136 | } |
137 | } finally { |
138 | in.close(); |
139 | } |
140 | files.put(path, content); |
141 | } |
142 | } |
143 | if (content != null) { |
144 | resp.setStatus(200); |
145 | String mimeType = mimeTypeFor(content.type, path); |
146 | if (mimeType != null) { |
147 | resp.setContentType(mimeType); |
148 | } |
149 | resp.setDateHeader("Last-modified", startupTime); |
150 | resp.setDateHeader("Expires", expiryDate); |
151 | if (content.isText()) { |
152 | Writer out = resp.getWriter(); |
153 | content.toWriter(out); |
154 | out.close(); |
155 | } else { |
156 | OutputStream out = resp.getOutputStream(); |
157 | content.toOutputStream(out); |
158 | out.close(); |
159 | } |
160 | } else { |
161 | resp.setStatus(404); |
162 | resp.setContentType("text/plain"); |
163 | Writer out = resp.getWriter(); |
164 | out.write("404 - I have no response to that."); |
165 | out.close(); |
166 | } |
167 | } |
168 | |
169 | private static String mimeTypeFor(ContentType t, String path) { |
170 | if (t != null) { |
171 | return t.isText ? t.mimeType + "; charset=UTF-8" : t.mimeType; |
172 | } |
173 | int dot = path.lastIndexOf('.'); |
174 | if (dot >= 0) { |
175 | String ext = path.substring(dot + 1); |
176 | if ("gif".equals(ext)) { return "image/gif"; } |
177 | if ("png".equals(ext)) { return "image/png"; } |
178 | if ("jpg".equals(ext)) { return "image/jpeg"; } |
179 | } |
180 | return null; |
181 | } |
182 | |
183 | private static <K, V> Map<K, V> mruCache(final int maxSize) { |
184 | return Collections.synchronizedMap(new LinkedHashMap<K, V>() { |
185 | @Override |
186 | public boolean removeEldestEntry(Map.Entry<K, V> e) { |
187 | return size() > maxSize; |
188 | } |
189 | }); |
190 | } |
191 | } |