Subversion Repository Public Repository

litesoft

Diff Revisions 949 vs 950 for /trunk/Java/VersionedStaticContentFilter/src/org/litesoft/servlet/versionedstaticcontentfilter/VersionedStaticContentFilter.java

Diff revisions: vs.
  @@ -1,354 +1,354 @@
1 - // This Source Code is in the Public Domain per: http://unlicense.org
2 - package org.litesoft.servlet.versionedstaticcontentfilter;
3 -
4 - import org.litesoft.servlet.WebInfLocator;
5 -
6 - import javax.servlet.Filter;
7 - import javax.servlet.FilterChain;
8 - import javax.servlet.FilterConfig;
9 - import javax.servlet.ServletContext;
10 - import javax.servlet.ServletException;
11 - import javax.servlet.ServletRequest;
12 - import javax.servlet.ServletResponse;
13 - import javax.servlet.http.HttpServletRequest;
14 - import javax.servlet.http.HttpServletResponse;
15 - import java.io.Closeable;
16 - import java.io.File;
17 - import java.io.FileInputStream;
18 - import java.io.IOException;
19 - import java.io.InputStream;
20 - import java.io.OutputStream;
21 - import java.util.ArrayList;
22 - import java.util.List;
23 -
24 - /**
25 - * This is a filter which enforces proper caching of a "LiteSoft Versioned" web app
26 - * w/ "common" redirection. It requires that you serve your GWT application via a
27 - * Java servlet container.
28 - * <p/>
29 - * Many thanks to Mat Gessel <mat.gessel@gmail.com> for providing an implementation of
30 - * the standard GWT Caching mechanism as a basis.
31 - * <p/>
32 - * To use, add the jar to <code>WEB-INF/lib</code> and add the
33 - * following to your deployment descriptor (web.xml):
34 - * <p/>
35 - * <pre>
36 - * &lt;filter&gt;
37 - * &lt;filter-name&gt;VersionedStaticContentFilter&lt;/filter-name&gt;
38 - * &lt;filter-class&gt;org.litesoft.servlet.versionedstaticcontentfilter.VersionedStaticContentFilter&lt;/filter-class&gt;
39 - * &lt;/filter&gt;
40 - *
41 - * &lt;filter-mapping&gt;
42 - * &lt;filter-name&gt;VersionedStaticContentFilter&lt;/filter-name&gt;
43 - * &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
44 - * &lt;/filter-mapping&gt;</pre>
45 - *
46 - * A "LiteSoft Versioned" web app is one where the majority of the DOM is built via
47 - * JavaScript (e.g. a GWT app) and that the path has a version number in it (e.g. /v1/).
48 - * This means that everything can be cached "forever" EXCEPT certain JavaScript (e.g.
49 - * the versioned bootstrap JavaScript). So this code set the caching headers to "forever"
50 - * for everything EXCEPT files that end with ".nocache.js".
51 - *
52 - * Additionally, it maps requests to a common path under the versioned path to just the
53 - * common path (e.g. "/v1/common/images/fred.jpg" becomes "/common/images/fred.jpg"). This
54 - * makes it so that when the app is "versioned", all the common resources do not need to be
55 - * duplicated for each version. Note: this also means that older versions will be accessing
56 - * the newer resources, but as the proper behavior of the older versions (in a "LiteSoft
57 - * Versioned" web app) are to immediately forward to the current version, there should be
58 - * little to no deleterious effects.
59 - *
60 - * Usage notes
61 - * <ul>
62 - * <li>You can verify that the filter is being applied with Firefox's Web
63 - * Developer Extension. Click Tools > Web Developer > Information > View
64 - * Response Headers.
65 - * <li>If you are running an Apache httpd/Jk/Tomcat server configuration you
66 - * need to ensure that Tomcat is serving HTML files, otherwise the filter will
67 - * not be applied.
68 - * <li>One reason that this filter exists is that you cannot use <code>*.nocache.html</code> or
69 - * <code>*.cache.html</code> for url patterns. According to the 2.3 servlet
70 - * spec, an extension is defined as the characters after the <strong>last</strong>
71 - * period.
72 - * <li>The header is modified <em>before</em> passing control down the filter chain.
73 - * </ul>
74 - *
75 - * @see <a
76 - * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9">Cache-control
77 - * directive</a>
78 - * @see <a
79 - * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21">Expires
80 - * directive</a>
81 - * @see <a
82 - * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.32">Pragma
83 - * directive</a>
84 - */
85 - public class VersionedStaticContentFilter implements Filter {
86 - public static final String VERSION_PATH_ID = "/ver";
87 -
88 - @Override
89 - public void init(FilterConfig filterConfig)
90 - throws ServletException {
91 - mServletContext = filterConfig.getServletContext();
92 - String zLocationAnchorPath = WebInfLocator.getLocationAnchorPath();
93 - String zDevMode = System.getProperty("DevMode");
94 - if (zDevMode == null) {
95 - mMode = new Production();
96 - } else {
97 - File[] zStaticFileSearchPaths = createStaticFileSearchPaths(zDevMode, zLocationAnchorPath);
98 - mMode = (zStaticFileSearchPaths == null) ? new DevMode() : new DevModeDirect(zStaticFileSearchPaths);
99 - }
100 - System.out.println("VersionedStaticContentFilter.init: " + zLocationAnchorPath + "\n Mode: " + mMode);
101 - }
102 -
103 - private File[] createStaticFileSearchPaths(String pDevMode, String pLocationAnchorPath) {
104 - File zDir = WebInfLocator.extractDir(pLocationAnchorPath);
105 - if (zDir == null) {
106 - return null;
107 - }
108 - File zWarDir = zDir.getParentFile();
109 - List<File> zStaticFileSearchPaths = new ArrayList<File>();
110 - zStaticFileSearchPaths.add(zWarDir);
111 - int from = 0;
112 - for (int at; -1 != (at = pDevMode.indexOf('|', from)); from = at + 1) {
113 - addPath(zStaticFileSearchPaths, zWarDir, pDevMode.substring(from, at));
114 - }
115 - addPath(zStaticFileSearchPaths, zWarDir, pDevMode.substring(from));
116 - return (zStaticFileSearchPaths.size() > 1) ? zStaticFileSearchPaths.toArray(new File[zStaticFileSearchPaths.size()]) : null;
117 - }
118 -
119 - private void addPath(List<File> pStaticFileSearchPaths, File pWarDir, String pPossiblePath) {
120 - if ((pPossiblePath = pPossiblePath.trim()).length() == 0) {
121 - return;
122 - }
123 - File zDir = new File(pWarDir, pPossiblePath);
124 - if (!zDir.isDirectory()) {
125 - System.out.println("VersionedStaticContentFilter.addPath, Not a Directory: " + zDir.getPath());
126 - return;
127 - }
128 - try {
129 - pStaticFileSearchPaths.add(zDir.getCanonicalFile());
130 - } catch (IOException e) {
131 - System.out.println("VersionedStaticContentFilter.addPath, Could not Canonicalize: " + zDir.getPath());
132 - }
133 - }
134 -
135 - private interface Mode {
136 - void processGetRequest(String pRequestURI, HttpServletRequest pRequest, HttpServletResponse pResponse, ServletContext pServletContext,
137 - FilterChain pChain)
138 - throws IOException, ServletException;
139 - }
140 -
141 - private Mode mMode;
142 - private ServletContext mServletContext;
143 -
144 - @Override
145 - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
146 - throws IOException, ServletException {
147 - if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse)) {
148 - HttpServletRequest zRequest = (HttpServletRequest) request;
149 - if ("GET".equals(zRequest.getMethod())) {
150 - String zURI = zRequest.getRequestURI();
151 - HttpServletResponse zResponse = (HttpServletResponse) response;
152 - mMode.processGetRequest(zURI, zRequest, zResponse, mServletContext, chain);
153 - return;
154 - }
155 - }
156 - chain.doFilter(request, response); // Goes to default servlet.
157 - }
158 -
159 - @Override
160 - public void destroy() {
161 - }
162 -
163 - public static class Production implements Mode {
164 - @Override
165 - public final void processGetRequest(String pRequestURI, HttpServletRequest pRequest, HttpServletResponse pResponse, ServletContext pServletContext,
166 - FilterChain pChain)
167 - throws IOException, ServletException {
168 - String zURI = cleanRequest(pRequestURI = pRequestURI.trim());
169 - if (shouldCacheExtensions(getLowerCaseExtension(zURI)) && isVersionedPath(zURI)) {
170 - cacheForever(pResponse);
171 - } else {
172 - noCache(pResponse);
173 - }
174 - delegate(pRequestURI, pRequest, pResponse, pServletContext, pChain);
175 - }
176 -
177 - private boolean isVersionedPath(String pCleanedRequestURI) {
178 - if (pCleanedRequestURI.startsWith(VERSION_PATH_ID)) // Starts w/ == Happy Case!
179 - {
180 - return true;
181 - }
182 - int zAt = pCleanedRequestURI.indexOf(VERSION_PATH_ID);
183 - return (zAt != -1) && // Has it in path
184 - (0 == pCleanedRequestURI.substring(0, zAt).lastIndexOf('/')); // AND is first sub-path (first being the WAR/directory name)
185 - }
186 -
187 - protected boolean shouldCacheExtensions(String pLowerCaseExtension) {
188 - return !"html".equals(pLowerCaseExtension);
189 - }
190 -
191 - protected String cleanRequest(String pRequestURI) {
192 - int at;
193 - if (-1 != (at = pRequestURI.indexOf('?'))) {
194 - return pRequestURI.substring(0, at);
195 - }
196 - if (-1 != (at = pRequestURI.indexOf('#'))) {
197 - return pRequestURI.substring(0, at);
198 - }
199 - return pRequestURI;
200 - }
201 -
202 - protected String getLowerCaseExtension(String pCleanedRequestURI) {
203 - int at = pCleanedRequestURI.lastIndexOf('.');
204 - return (-1 == at) ? "" : pCleanedRequestURI.substring(at + 1).toLowerCase();
205 - }
206 -
207 - protected void delegate(String pRequestURI, HttpServletRequest pRequest, HttpServletResponse pResponse,
208 - ServletContext pServletContext, FilterChain pChain)
209 - throws ServletException, IOException {
210 - pChain.doFilter(pRequest, pResponse); // Goes to default servlet.
211 - }
212 -
213 - @Override
214 - public String toString() {
215 - return "Production";
216 - }
217 - }
218 -
219 - public static class DevMode extends Production {
220 - @Override
221 - protected boolean shouldCacheExtensions(String pLowerCaseExtension) {
222 - return super.shouldCacheExtensions(pLowerCaseExtension) && !"js".equals(pLowerCaseExtension) && !"css".equals(pLowerCaseExtension);
223 - }
224 -
225 - @Override
226 - public String toString() {
227 - return "Dev";
228 - }
229 - }
230 -
231 - public static class DevModeDirect extends DevMode {
232 - private File[] mStaticFileSearchPaths;
233 -
234 - public DevModeDirect(File[] pStaticFileSearchPaths) {
235 - mStaticFileSearchPaths = pStaticFileSearchPaths;
236 - }
237 -
238 - @Override
239 - protected void delegate(String pRequestURI, HttpServletRequest pRequest, HttpServletResponse pResponse,
240 - ServletContext pServletContext, FilterChain pChain)
241 - throws ServletException, IOException {
242 - File zFoundFile;
243 - String mimeType;
244 - synchronized (this) { // Just for Dev mode to force the Console to not intersperse
245 - if (null == (zFoundFile = findFile(pRequestURI))) {
246 - pResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
247 - return;
248 - }
249 - // Get the MIME type of the image
250 - if (null == (mimeType = pServletContext.getMimeType(zFoundFile.getName().toLowerCase()))) {
251 - System.out.print(" | Could not get MIME type of " + zFoundFile.getName());
252 - mimeType = "application/octet-stream";
253 - }
254 - System.out.println();
255 - }
256 -
257 - // Set content type
258 - pResponse.setContentType(mimeType);
259 -
260 - // Set content size
261 - pResponse.setContentLength((int) zFoundFile.length());
262 -
263 - // Open the file and output streams
264 - InputStream in = new FileInputStream(zFoundFile);
265 - try {
266 - OutputStream out = pResponse.getOutputStream();
267 - try {
268 - copyStream(in, out);
269 -
270 - Closeable zCloseable = out;
271 - out = null;
272 - zCloseable.close();
273 - } finally {
274 - closeQuietly(out);
275 - }
276 - } finally {
277 - closeQuietly(in);
278 - }
279 - }
280 -
281 - private File findFile(String pRequestURI) {
282 - for (File zPath : mStaticFileSearchPaths) {
283 - File zFound = checkPath(zPath, pRequestURI);
284 - if (zFound != null) {
285 - System.out.print("VersionedStaticContentFilter.FindFile (200): " + pRequestURI + " -> " + zFound.getPath());
286 - return zFound;
287 - }
288 - }
289 - System.out.println("VersionedStaticContentFilter.FindFile (404): " + pRequestURI);
290 - return null;
291 - }
292 -
293 - @Override
294 - public String toString() {
295 - StringBuilder sb = new StringBuilder().append(super.toString()).append(':');
296 - for (File zPath : mStaticFileSearchPaths) {
297 - sb.append("\n ").append(zPath.getPath());
298 - }
299 - return sb.toString();
300 - }
301 - }
302 -
303 - private static void closeQuietly(Closeable pClosable) {
304 - if (pClosable != null) {
305 - try {
306 - pClosable.close();
307 - } catch (IOException e) {
308 - // Whatever
309 - }
310 - }
311 - }
312 -
313 - private static void copyStream(InputStream pIn, OutputStream pOut)
314 - throws IOException {
315 - // Copy the contents of the file to the output stream
316 - byte[] buf = new byte[1024];
317 - for (int count; (count = pIn.read(buf)) > 0; ) {
318 - pOut.write(buf, 0, count);
319 - }
320 - }
321 -
322 - private static File checkPath(File pPath, String pSubPath) {
323 - if (pSubPath == null || pSubPath.length() == 0) {
324 - return null;
325 - }
326 - File zFile = new File(pPath, pSubPath);
327 - if (!zFile.isFile()) {
328 - return null;
329 - }
330 - try {
331 - return zFile.getCanonicalFile();
332 - } catch (IOException e) {
333 - e.printStackTrace();
334 - return null;
335 - }
336 - }
337 -
338 - private static void cacheForever(HttpServletResponse pResponse) {
339 - // the w3c spec requires a maximum age of 1 year
340 - // Firefox 3+ needs 'public' to cache this resource when received via SSL
341 - pResponse.setHeader("Cache-Control", "public max-age=31536000");
342 -
343 - // necessary to overwrite "Pragma: no-cache" header
344 - pResponse.setHeader("Pragma", "temp");
345 - pResponse.setHeader("Pragma", "");
346 - pResponse.setDateHeader("Expires", System.currentTimeMillis() + 31536000000l);
347 - }
348 -
349 - private static void noCache(HttpServletResponse pResponse) {
350 - pResponse.setHeader("Cache-Control", "no-cache no-store must-revalidate");
351 - pResponse.setHeader("Pragma", "no-cache"); // HTTP/1.0
352 - pResponse.setDateHeader("Expires", 86400000); // January 2, 1970
353 - }
354 - }
1 + // This Source Code is in the Public Domain per: http://unlicense.org
2 + package org.litesoft.servlet.versionedstaticcontentfilter;
3 +
4 + import org.litesoft.servlet.WebInfLocator;
5 +
6 + import javax.servlet.Filter;
7 + import javax.servlet.FilterChain;
8 + import javax.servlet.FilterConfig;
9 + import javax.servlet.ServletContext;
10 + import javax.servlet.ServletException;
11 + import javax.servlet.ServletRequest;
12 + import javax.servlet.ServletResponse;
13 + import javax.servlet.http.HttpServletRequest;
14 + import javax.servlet.http.HttpServletResponse;
15 + import java.io.Closeable;
16 + import java.io.File;
17 + import java.io.FileInputStream;
18 + import java.io.IOException;
19 + import java.io.InputStream;
20 + import java.io.OutputStream;
21 + import java.util.ArrayList;
22 + import java.util.List;
23 +
24 + /**
25 + * This is a filter which enforces proper caching of a "LiteSoft Versioned" web app
26 + * w/ "common" redirection. It requires that you serve your GWT application via a
27 + * Java servlet container.
28 + * <p/>
29 + * Many thanks to Mat Gessel <mat.gessel@gmail.com> for providing an implementation of
30 + * the standard GWT Caching mechanism as a basis.
31 + * <p/>
32 + * To use, add the jar to <code>WEB-INF/lib</code> and add the
33 + * following to your deployment descriptor (web.xml):
34 + * <p/>
35 + * <pre>
36 + * &lt;filter&gt;
37 + * &lt;filter-name&gt;VersionedStaticContentFilter&lt;/filter-name&gt;
38 + * &lt;filter-class&gt;org.litesoft.servlet.versionedstaticcontentfilter.VersionedStaticContentFilter&lt;/filter-class&gt;
39 + * &lt;/filter&gt;
40 + *
41 + * &lt;filter-mapping&gt;
42 + * &lt;filter-name&gt;VersionedStaticContentFilter&lt;/filter-name&gt;
43 + * &lt;url-pattern&gt;/*&lt;/url-pattern&gt;
44 + * &lt;/filter-mapping&gt;</pre>
45 + *
46 + * A "LiteSoft Versioned" web app is one where the majority of the DOM is built via
47 + * JavaScript (e.g. a GWT app) and that the path has a version number in it (e.g. /v1/).
48 + * This means that everything can be cached "forever" EXCEPT certain JavaScript (e.g.
49 + * the versioned bootstrap JavaScript). So this code set the caching headers to "forever"
50 + * for everything EXCEPT files that end with ".nocache.js".
51 + *
52 + * Additionally, it maps requests to a common path under the versioned path to just the
53 + * common path (e.g. "/v1/common/images/fred.jpg" becomes "/common/images/fred.jpg"). This
54 + * makes it so that when the app is "versioned", all the common resources do not need to be
55 + * duplicated for each version. Note: this also means that older versions will be accessing
56 + * the newer resources, but as the proper behavior of the older versions (in a "LiteSoft
57 + * Versioned" web app) are to immediately forward to the current version, there should be
58 + * little to no deleterious effects.
59 + *
60 + * Usage notes
61 + * <ul>
62 + * <li>You can verify that the filter is being applied with Firefox's Web
63 + * Developer Extension. Click Tools > Web Developer > Information > View
64 + * Response Headers.
65 + * <li>If you are running an Apache httpd/Jk/Tomcat server configuration you
66 + * need to ensure that Tomcat is serving HTML files, otherwise the filter will
67 + * not be applied.
68 + * <li>One reason that this filter exists is that you cannot use <code>*.nocache.html</code> or
69 + * <code>*.cache.html</code> for url patterns. According to the 2.3 servlet
70 + * spec, an extension is defined as the characters after the <strong>last</strong>
71 + * period.
72 + * <li>The header is modified <em>before</em> passing control down the filter chain.
73 + * </ul>
74 + *
75 + * @see <a
76 + * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9">Cache-control
77 + * directive</a>
78 + * @see <a
79 + * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21">Expires
80 + * directive</a>
81 + * @see <a
82 + * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.32">Pragma
83 + * directive</a>
84 + */
85 + public class VersionedStaticContentFilter implements Filter {
86 + public static final String VERSION_PATH_ID = "/ver";
87 +
88 + @Override
89 + public void init(FilterConfig filterConfig)
90 + throws ServletException {
91 + mServletContext = filterConfig.getServletContext();
92 + String zLocationAnchorPath = WebInfLocator.getLocationAnchorPath();
93 + String zDevMode = System.getProperty("DevMode");
94 + if (zDevMode == null) {
95 + mMode = new Production();
96 + } else {
97 + File[] zStaticFileSearchPaths = createStaticFileSearchPaths(zDevMode, zLocationAnchorPath);
98 + mMode = (zStaticFileSearchPaths == null) ? new DevMode() : new DevModeDirect(zStaticFileSearchPaths);
99 + }
100 + System.out.println("VersionedStaticContentFilter.init: " + zLocationAnchorPath + "\n Mode: " + mMode);
101 + }
102 +
103 + private File[] createStaticFileSearchPaths(String pDevMode, String pLocationAnchorPath) {
104 + File zDir = WebInfLocator.extractDir(pLocationAnchorPath);
105 + if (zDir == null) {
106 + return null;
107 + }
108 + File zWarDir = zDir.getParentFile();
109 + List<File> zStaticFileSearchPaths = new ArrayList<File>();
110 + zStaticFileSearchPaths.add(zWarDir);
111 + int from = 0;
112 + for (int at; -1 != (at = pDevMode.indexOf('|', from)); from = at + 1) {
113 + addPath(zStaticFileSearchPaths, zWarDir, pDevMode.substring(from, at));
114 + }
115 + addPath(zStaticFileSearchPaths, zWarDir, pDevMode.substring(from));
116 + return (zStaticFileSearchPaths.size() > 1) ? zStaticFileSearchPaths.toArray(new File[zStaticFileSearchPaths.size()]) : null;
117 + }
118 +
119 + private void addPath(List<File> pStaticFileSearchPaths, File pWarDir, String pPossiblePath) {
120 + if ((pPossiblePath = pPossiblePath.trim()).length() == 0) {
121 + return;
122 + }
123 + File zDir = new File(pWarDir, pPossiblePath);
124 + if (!zDir.isDirectory()) {
125 + System.out.println("VersionedStaticContentFilter.addPath, Not a Directory: " + zDir.getPath());
126 + return;
127 + }
128 + try {
129 + pStaticFileSearchPaths.add(zDir.getCanonicalFile());
130 + } catch (IOException e) {
131 + System.out.println("VersionedStaticContentFilter.addPath, Could not Canonicalize: " + zDir.getPath());
132 + }
133 + }
134 +
135 + private interface Mode {
136 + void processGetRequest(String pRequestURI, HttpServletRequest pRequest, HttpServletResponse pResponse, ServletContext pServletContext,
137 + FilterChain pChain)
138 + throws IOException, ServletException;
139 + }
140 +
141 + private Mode mMode;
142 + private ServletContext mServletContext;
143 +
144 + @Override
145 + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
146 + throws IOException, ServletException {
147 + if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse)) {
148 + HttpServletRequest zRequest = (HttpServletRequest) request;
149 + if ("GET".equals(zRequest.getMethod())) {
150 + String zURI = zRequest.getRequestURI();
151 + HttpServletResponse zResponse = (HttpServletResponse) response;
152 + mMode.processGetRequest(zURI, zRequest, zResponse, mServletContext, chain);
153 + return;
154 + }
155 + }
156 + chain.doFilter(request, response); // Goes to default servlet.
157 + }
158 +
159 + @Override
160 + public void destroy() {
161 + }
162 +
163 + public static class Production implements Mode {
164 + @Override
165 + public final void processGetRequest(String pRequestURI, HttpServletRequest pRequest, HttpServletResponse pResponse, ServletContext pServletContext,
166 + FilterChain pChain)
167 + throws IOException, ServletException {
168 + String zURI = cleanRequest(pRequestURI = pRequestURI.trim());
169 + if (shouldCacheExtensions(getLowerCaseExtension(zURI)) && isVersionedPath(zURI)) {
170 + cacheForever(pResponse);
171 + } else {
172 + noCache(pResponse);
173 + }
174 + delegate(pRequestURI, pRequest, pResponse, pServletContext, pChain);
175 + }
176 +
177 + private boolean isVersionedPath(String pCleanedRequestURI) {
178 + if (pCleanedRequestURI.startsWith(VERSION_PATH_ID)) // Starts w/ == Happy Case!
179 + {
180 + return true;
181 + }
182 + int zAt = pCleanedRequestURI.indexOf(VERSION_PATH_ID);
183 + return (zAt != -1) && // Has it in path
184 + (0 == pCleanedRequestURI.substring(0, zAt).lastIndexOf('/')); // AND is first sub-path (first being the WAR/directory name)
185 + }
186 +
187 + protected boolean shouldCacheExtensions(String pLowerCaseExtension) {
188 + return !"html".equals(pLowerCaseExtension);
189 + }
190 +
191 + protected String cleanRequest(String pRequestURI) {
192 + int at;
193 + if (-1 != (at = pRequestURI.indexOf('?'))) {
194 + return pRequestURI.substring(0, at);
195 + }
196 + if (-1 != (at = pRequestURI.indexOf('#'))) {
197 + return pRequestURI.substring(0, at);
198 + }
199 + return pRequestURI;
200 + }
201 +
202 + protected String getLowerCaseExtension(String pCleanedRequestURI) {
203 + int at = pCleanedRequestURI.lastIndexOf('.');
204 + return (-1 == at) ? "" : pCleanedRequestURI.substring(at + 1).toLowerCase();
205 + }
206 +
207 + protected void delegate(String pRequestURI, HttpServletRequest pRequest, HttpServletResponse pResponse,
208 + ServletContext pServletContext, FilterChain pChain)
209 + throws ServletException, IOException {
210 + pChain.doFilter(pRequest, pResponse); // Goes to default servlet.
211 + }
212 +
213 + @Override
214 + public String toString() {
215 + return "Production";
216 + }
217 + }
218 +
219 + public static class DevMode extends Production {
220 + @Override
221 + protected boolean shouldCacheExtensions(String pLowerCaseExtension) {
222 + return super.shouldCacheExtensions(pLowerCaseExtension) && !"js".equals(pLowerCaseExtension) && !"css".equals(pLowerCaseExtension);
223 + }
224 +
225 + @Override
226 + public String toString() {
227 + return "Dev";
228 + }
229 + }
230 +
231 + public static class DevModeDirect extends DevMode {
232 + private File[] mStaticFileSearchPaths;
233 +
234 + public DevModeDirect(File[] pStaticFileSearchPaths) {
235 + mStaticFileSearchPaths = pStaticFileSearchPaths;
236 + }
237 +
238 + @Override
239 + protected void delegate(String pRequestURI, HttpServletRequest pRequest, HttpServletResponse pResponse,
240 + ServletContext pServletContext, FilterChain pChain)
241 + throws ServletException, IOException {
242 + File zFoundFile;
243 + String mimeType;
244 + synchronized (this) { // Just for Dev mode to force the Console to not intersperse
245 + if (null == (zFoundFile = findFile(pRequestURI))) {
246 + pResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
247 + return;
248 + }
249 + // Get the MIME type of the image
250 + if (null == (mimeType = pServletContext.getMimeType(zFoundFile.getName().toLowerCase()))) {
251 + System.out.print(" | Could not get MIME type of " + zFoundFile.getName());
252 + mimeType = "application/octet-stream";
253 + }
254 + System.out.println();
255 + }
256 +
257 + // Set content type
258 + pResponse.setContentType(mimeType);
259 +
260 + // Set content size
261 + pResponse.setContentLength((int) zFoundFile.length());
262 +
263 + // Open the file and output streams
264 + InputStream in = new FileInputStream(zFoundFile);
265 + try {
266 + OutputStream out = pResponse.getOutputStream();
267 + try {
268 + copyStream(in, out);
269 +
270 + Closeable zCloseable = out;
271 + out = null;
272 + zCloseable.close();
273 + } finally {
274 + closeQuietly(out);
275 + }
276 + } finally {
277 + closeQuietly(in);
278 + }
279 + }
280 +
281 + private File findFile(String pRequestURI) {
282 + for (File zPath : mStaticFileSearchPaths) {
283 + File zFound = checkPath(zPath, pRequestURI);
284 + if (zFound != null) {
285 + System.out.print("VersionedStaticContentFilter.FindFile (200): " + pRequestURI + " -> " + zFound.getPath());
286 + return zFound;
287 + }
288 + }
289 + System.out.println("VersionedStaticContentFilter.FindFile (404): " + pRequestURI);
290 + return null;
291 + }
292 +
293 + @Override
294 + public String toString() {
295 + StringBuilder sb = new StringBuilder().append(super.toString()).append(':');
296 + for (File zPath : mStaticFileSearchPaths) {
297 + sb.append("\n ").append(zPath.getPath());
298 + }
299 + return sb.toString();
300 + }
301 + }
302 +
303 + private static void closeQuietly(Closeable pClosable) {
304 + if (pClosable != null) {
305 + try {
306 + pClosable.close();
307 + } catch (IOException e) {
308 + // Whatever
309 + }
310 + }
311 + }
312 +
313 + private static void copyStream(InputStream pIn, OutputStream pOut)
314 + throws IOException {
315 + // Copy the contents of the file to the output stream
316 + byte[] buf = new byte[1024];
317 + for (int count; (count = pIn.read(buf)) > 0; ) {
318 + pOut.write(buf, 0, count);
319 + }
320 + }
321 +
322 + private static File checkPath(File pPath, String pSubPath) {
323 + if (pSubPath == null || pSubPath.length() == 0) {
324 + return null;
325 + }
326 + File zFile = new File(pPath, pSubPath);
327 + if (!zFile.isFile()) {
328 + return null;
329 + }
330 + try {
331 + return zFile.getCanonicalFile();
332 + } catch (IOException e) {
333 + e.printStackTrace();
334 + return null;
335 + }
336 + }
337 +
338 + private static void cacheForever(HttpServletResponse pResponse) {
339 + // the w3c spec requires a maximum age of 1 year
340 + // Firefox 3+ needs 'public' to cache this resource when received via SSL
341 + pResponse.setHeader("Cache-Control", "public max-age=31536000");
342 +
343 + // necessary to overwrite "Pragma: no-cache" header
344 + pResponse.setHeader("Pragma", "temp");
345 + pResponse.setHeader("Pragma", "");
346 + pResponse.setDateHeader("Expires", System.currentTimeMillis() + 31536000000l);
347 + }
348 +
349 + private static void noCache(HttpServletResponse pResponse) {
350 + pResponse.setHeader("Cache-Control", "no-cache no-store must-revalidate");
351 + pResponse.setHeader("Pragma", "no-cache"); // HTTP/1.0
352 + pResponse.setDateHeader("Expires", 86400000); // January 2, 1970
353 + }
354 + }