View Javadoc

1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    *      http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  package gr.abiss.mvn.plugins.jstools.web;
15  
16  import gr.abiss.mvn.plugins.jstools.utils.ClasspathResourceUtils;
17  import java.io.ByteArrayOutputStream;
18  import java.io.IOException;
19  import java.util.Collections;
20  import java.util.zip.GZIPOutputStream;
21  import java.util.HashMap;
22  import java.util.HashSet;
23  import java.util.Map;
24  import java.util.Set;
25  import java.util.concurrent.ConcurrentHashMap;
26  import javax.servlet.Filter;
27  import javax.servlet.FilterChain;
28  import javax.servlet.FilterConfig;
29  import javax.servlet.ServletException;
30  import javax.servlet.ServletOutputStream;
31  import javax.servlet.ServletRequest;
32  import javax.servlet.ServletResponse;
33  import javax.servlet.http.HttpServletRequest;
34  import javax.servlet.http.HttpServletResponse;
35  import org.apache.commons.lang.BooleanUtils;
36  import org.apache.log4j.Logger;
37  
38  /***
39   * A Servlet Filter that loads Javascript files or other static resources from
40   * the runtime classpath and uses them to build an HTTP response. Web
41   * applications can, for example, include JS dependencies in their Maven POM and
42   * the filter will resolve those at runtime, cache them and produce the
43   * appropriate HTTP response.
44   * 
45   * The filter will only serve classpath resource types with file extentions you
46   * configure it to, (see the <code>allowedExtentions</code> init-param) and
47   * with the Cache-Control HTTP header you specify (see the
48   * <code>cacheControl</code> init-param).
49   * 
50   * Note that the filter will only handle HTTP GET and HTTP HEAD requests. All
51   * other HTTP request methods will be server with a "Non Implemented" (501) HTTP
52   * response error.
53   * 
54   * To add the filter in your application, add the folowing in your web.xml or,
55   * in case you use XDoclet (webdoclet), your filters.xml merge file:
56   * 
57   * <pre>
58   *    &lt;filter&gt; 
59   *       &lt;filter-name&gt;JavascriptDependencyFilter&lt;/filter-name&gt;
60   *       &lt;filter-class&gt;gr.abiss.mvn.plugins.jstools.web.JavascriptDependencyFilter&lt;/filter-class&gt;
61   *       &lt;init-param&gt; 
62   *          &lt;!-- 
63   *             Optional, tells the filter which file extentions are allowed.
64   *             The default are the sample values below (if you just want those,
65   *             you don't have to configure the filter).
66   *             Note that the Filter will NOT load resources from META-INF.
67   *          --&gt; 
68   *          &lt;param-name&gt;<strong>allowedExtentions</strong>&lt;/param-name&gt;
69   *          &lt;param-value&gt;js png jpg gif txt html htm xml xsl xslt svg svgz swf&lt;/param-value&gt; 
70   *      &lt;/init-param&gt;
71   *       &lt;init-param&gt; 
72   *          &lt;!-- 
73   *             Optional, used to configure the Cache-Control HTTP header. 
74   *             Default is &quot;max-age=86400&quot; 
75   *          --&gt; 
76   *          &lt;param-name&gt;<strong>cacheControl</strong>&lt;/param-name&gt;
77   *          &lt;param-value&gt;max-age=86400&lt;/param-value&gt; 
78   *      &lt;/init-param&gt;
79   *       &lt;init-param&gt; 
80   *          &lt;!-- 
81   *             Optional, enable gzip compression for browsers that support it
82   *             (based on HTTP request headers). Default is true.
83   *          --&gt; 
84   *          &lt;param-name&gt;<strong>enableGzip</strong>&lt;/param-name&gt;
85   *          &lt;param-value&gt;true&lt;/param-value&gt; 
86   *      &lt;/init-param&gt;
87   *       &lt;init-param&gt; 
88   *          &lt;!-- 
89   *             Optional, control whether resources are cached to
90   *             avoid I/O overhead every time they are requested. Default is true.
91   *          --&gt; 
92   *          &lt;param-name&gt;<strong>enableCache</strong>&lt;/param-name&gt;
93   *          &lt;param-value&gt;true&lt;/param-value&gt; 
94   *      &lt;/init-param&gt;
95   *       &lt;init-param&gt; 
96   *          &lt;!-- 
97   *             Optional, control whether gzipped resources are cached to
98   *             avoid GZIPing overhead every time they are requested. Default is true.
99   *          --&gt; 
100  *          &lt;param-name&gt;<strong>enableGzipedCache</strong>&lt;/param-name&gt;
101  *          &lt;param-value&gt;true&lt;/param-value&gt; 
102  *      &lt;/init-param&gt;
103  *      &lt;init-param&gt;
104  *          &lt;!-- 
105  *             Optional, tells the filter whether to send an HTTP 404 (&quot;not found&quot;) code 
106  *             or let go of the filter chain to the application if no matching resource
107  *             is found in the classpath. Possible values are {true, false}.
108  *             The default is to send a 404 (true). 
109  *          --&gt; 
110  *          &lt;param-name&gt;<strong>send404</strong>&lt;/param-name&gt;
111  *          &lt;param-value&gt;true&lt;/param-value&gt; 
112  *      &lt;/init-param&gt;
113  *       &lt;init-param&gt; 
114  *          &lt;!--
115  *             Optional (mandatory for arbitary url-pattern in filter-mapping). 
116  *             Used to tell the plugin what base URL fragment to ignore when looking 
117  *             for Javascript resources. Must match the url-pattern in filter-mapping, 
118  *             see below. Default is /lib/js
119  *          --&gt; 
120  *          &lt;param-name&gt;<strong>basePath</strong>&lt;/param-name&gt;
121  *          &lt;param-value&gt;/lib/js/&lt;/param-value&gt; 
122  *      &lt;/init-param&gt; 
123  *    &lt;/filter&gt;
124  * </pre>
125  * 
126  * The servlet container also needs a mapping for the filter. You really don't
127  * want the filter intercepting all HTTP requests! The URL pattern in the filter
128  * mapping <strong>must</strong> be exactly the same with the value of the
129  * <code>basePath</code> inialization parameter:
130  * 
131  * <pre>
132  *    &lt;filter-mapping&gt;
133  *      &lt;filter-name&gt;JavascriptDependencyFilter&lt;/filter-name&gt;
134  *      &lt;!-- matches the default value of the <strong>basePath</strong>
135  * <code>
136  * init - param
137  * </code>
138  *      &lt;url-pattern&gt;/lib/js/&lt;/url-pattern&gt;
139  *    &lt;/filter-mapping&gt;
140  * </pre>
141  * 
142  * This helps in inly invoking the filter for resources that are actually in the
143  * classpath. To clarify, suppose an HTML response has a script tag like this:
144  * 
145  * <pre>
146  *    &lt;script type=&quot;text/javascript&quot; 
147  *       src=&quot;/lib/js/gr/abiss/js/sarissa/sarissa.js&quot;&gt; 
148  *    &lt;script&amp;gt
149  * </pre>
150  * 
151  * The browser will try to load the script making an HTTP GET for that URL. The
152  * server will have the filter intercept the request since it matches the URL
153  * pattern of the filter mapping. The filter will then do the following:
154  * <ol>
155  * <li>Obtain the request URI</li>
156  * <li>Substring the URL removing the part that matches the value of the
157  * <code>basePath</code> initialization parameter</li>
158  * <li>Use the result to lookup the corresponding resource in it's cache. Load
159  * the resource and put it in the cache if not already there</li>
160  * <li>If the resource exists make an HTTP response with it, otherwise let the
161  * filter chain go or send a 404 depending on the value of the
162  * <code>send404</code> initialization parameter.</li>
163  * 
164  * @version $Id$
165  * @author manos
166  */
167 public class JavascriptDependencyFilter implements Filter {
168 	private static Logger log = Logger
169 			.getLogger(JavascriptDependencyFilter.class);
170 
171 	/***
172 	 * The knwon MIME types map for this filter (file extention, MIME type)
173 	 */
174 	protected static Map<String, String> MIME_TYPE_MAPPINGS = new HashMap<String, String>();
175 	static {
176 		MIME_TYPE_MAPPINGS.put("abs", "audio/x-mpeg");
177 		MIME_TYPE_MAPPINGS.put("ai", "application/postscript");
178 		MIME_TYPE_MAPPINGS.put("aif", "audio/x-aiff");
179 		MIME_TYPE_MAPPINGS.put("aifc", "audio/x-aiff");
180 		MIME_TYPE_MAPPINGS.put("aiff", "audio/x-aiff");
181 		MIME_TYPE_MAPPINGS.put("aim", "application/x-aim");
182 		MIME_TYPE_MAPPINGS.put("art", "image/x-jg");
183 		MIME_TYPE_MAPPINGS.put("asf", "video/x-ms-asf");
184 		MIME_TYPE_MAPPINGS.put("asx", "video/x-ms-asf");
185 		MIME_TYPE_MAPPINGS.put("au", "audio/basic");
186 		MIME_TYPE_MAPPINGS.put("avi", "video/x-msvideo");
187 		MIME_TYPE_MAPPINGS.put("avx", "video/x-rad-screenplay");
188 		MIME_TYPE_MAPPINGS.put("bcpio", "application/x-bcpio");
189 		MIME_TYPE_MAPPINGS.put("bin", "application/octet-stream");
190 		MIME_TYPE_MAPPINGS.put("bmp", "image/bmp");
191 		MIME_TYPE_MAPPINGS.put("body", "text/html");
192 		MIME_TYPE_MAPPINGS.put("cdf", "application/x-cdf");
193 		MIME_TYPE_MAPPINGS.put("cer", "application/x-x509-ca-cert");
194 		MIME_TYPE_MAPPINGS.put("class", "application/java");
195 		MIME_TYPE_MAPPINGS.put("cpio", "application/x-cpio");
196 		MIME_TYPE_MAPPINGS.put("csh", "application/x-csh");
197 		MIME_TYPE_MAPPINGS.put("css", "text/css");
198 		MIME_TYPE_MAPPINGS.put("dib", "image/bmp");
199 		MIME_TYPE_MAPPINGS.put("doc", "application/msword");
200 		MIME_TYPE_MAPPINGS.put("dtd", "text/plain");
201 		MIME_TYPE_MAPPINGS.put("dv", "video/x-dv");
202 		MIME_TYPE_MAPPINGS.put("dvi", "application/x-dvi");
203 		MIME_TYPE_MAPPINGS.put("eps", "application/postscript");
204 		MIME_TYPE_MAPPINGS.put("etx", "text/x-setext");
205 		MIME_TYPE_MAPPINGS.put("exe", "application/octet-stream");
206 		MIME_TYPE_MAPPINGS.put("gif", "image/gif");
207 		MIME_TYPE_MAPPINGS.put("gtar", "application/x-gtar");
208 		MIME_TYPE_MAPPINGS.put("gz", "application/x-gzip");
209 		MIME_TYPE_MAPPINGS.put("hdf", "application/x-hdf");
210 		MIME_TYPE_MAPPINGS.put("hqx", "application/mac-binhex40");
211 		MIME_TYPE_MAPPINGS.put("htc", "text/x-component");
212 		MIME_TYPE_MAPPINGS.put("htm", "text/html");
213 		MIME_TYPE_MAPPINGS.put("html", "text/html");
214 		MIME_TYPE_MAPPINGS.put("hqx", "application/mac-binhex40");
215 		MIME_TYPE_MAPPINGS.put("ief", "image/ief");
216 		MIME_TYPE_MAPPINGS.put("jad", "text/vnd.sun.j2me.app-descriptor");
217 		MIME_TYPE_MAPPINGS.put("jar", "application/java-archive");
218 		MIME_TYPE_MAPPINGS.put("java", "text/plain");
219 		MIME_TYPE_MAPPINGS.put("jnlp", "application/x-java-jnlp-file");
220 		MIME_TYPE_MAPPINGS.put("jpe", "image/jpeg");
221 		MIME_TYPE_MAPPINGS.put("jpeg", "image/jpeg");
222 		MIME_TYPE_MAPPINGS.put("jpg", "image/jpeg");
223 		MIME_TYPE_MAPPINGS.put("js", "text/javascript");
224 		MIME_TYPE_MAPPINGS.put("jsf", "text/plain");
225 		MIME_TYPE_MAPPINGS.put("jspf", "text/plain");
226 		MIME_TYPE_MAPPINGS.put("kar", "audio/x-midi");
227 		MIME_TYPE_MAPPINGS.put("latex", "application/x-latex");
228 		MIME_TYPE_MAPPINGS.put("m3u", "audio/x-mpegurl");
229 		MIME_TYPE_MAPPINGS.put("mac", "image/x-macpaint");
230 		MIME_TYPE_MAPPINGS.put("man", "application/x-troff-man");
231 		MIME_TYPE_MAPPINGS.put("me", "application/x-troff-me");
232 		MIME_TYPE_MAPPINGS.put("mid", "audio/x-midi");
233 		MIME_TYPE_MAPPINGS.put("midi", "audio/x-midi");
234 		MIME_TYPE_MAPPINGS.put("mif", "application/x-mif");
235 		MIME_TYPE_MAPPINGS.put("mov", "video/quicktime");
236 		MIME_TYPE_MAPPINGS.put("movie", "video/x-sgi-movie");
237 		MIME_TYPE_MAPPINGS.put("mp1", "audio/x-mpeg");
238 		MIME_TYPE_MAPPINGS.put("mp2", "audio/x-mpeg");
239 		MIME_TYPE_MAPPINGS.put("mp3", "audio/x-mpeg");
240 		MIME_TYPE_MAPPINGS.put("mpa", "audio/x-mpeg");
241 		MIME_TYPE_MAPPINGS.put("mpe", "video/mpeg");
242 		MIME_TYPE_MAPPINGS.put("mpeg", "video/mpeg");
243 		MIME_TYPE_MAPPINGS.put("mpega", "audio/x-mpeg");
244 		MIME_TYPE_MAPPINGS.put("mpg", "video/mpeg");
245 		MIME_TYPE_MAPPINGS.put("mpv2", "video/mpeg2");
246 		MIME_TYPE_MAPPINGS.put("ms", "application/x-wais-source");
247 		MIME_TYPE_MAPPINGS.put("nc", "application/x-netcdf");
248 		MIME_TYPE_MAPPINGS.put("oda", "application/oda");
249 		MIME_TYPE_MAPPINGS.put("pbm", "image/x-portable-bitmap");
250 		MIME_TYPE_MAPPINGS.put("pct", "image/pict");
251 		MIME_TYPE_MAPPINGS.put("pdf", "application/pdf");
252 		MIME_TYPE_MAPPINGS.put("pgm", "image/x-portable-graymap");
253 		MIME_TYPE_MAPPINGS.put("pic", "image/pict");
254 		MIME_TYPE_MAPPINGS.put("pict", "image/pict");
255 		MIME_TYPE_MAPPINGS.put("pls", "audio/x-scpls");
256 		MIME_TYPE_MAPPINGS.put("png", "image/png");
257 		MIME_TYPE_MAPPINGS.put("pnm", "image/x-portable-anymap");
258 		MIME_TYPE_MAPPINGS.put("pnt", "image/x-macpaint");
259 		MIME_TYPE_MAPPINGS.put("ppm", "image/x-portable-pixmap");
260 		MIME_TYPE_MAPPINGS.put("ps", "application/postscript");
261 		MIME_TYPE_MAPPINGS.put("psd", "image/x-photoshop");
262 		MIME_TYPE_MAPPINGS.put("qt", "video/quicktime");
263 		MIME_TYPE_MAPPINGS.put("qti", "image/x-quicktime");
264 		MIME_TYPE_MAPPINGS.put("qtif", "image/x-quicktime");
265 		MIME_TYPE_MAPPINGS.put("ras", "image/x-cmu-raster");
266 		MIME_TYPE_MAPPINGS.put("rgb", "image/x-rgb");
267 		MIME_TYPE_MAPPINGS.put("rm", "application/vnd.rn-realmedia");
268 		MIME_TYPE_MAPPINGS.put("roff", "application/x-troff");
269 		MIME_TYPE_MAPPINGS.put("rtf", "application/rtf");
270 		MIME_TYPE_MAPPINGS.put("rtx", "text/richtext");
271 		MIME_TYPE_MAPPINGS.put("sh", "application/x-sh");
272 		MIME_TYPE_MAPPINGS.put("shar", "application/x-shar");
273 		MIME_TYPE_MAPPINGS.put("smf", "audio/x-midi");
274 		MIME_TYPE_MAPPINGS.put("snd", "audio/basic");
275 		MIME_TYPE_MAPPINGS.put("src", "application/x-wais-source");
276 		MIME_TYPE_MAPPINGS.put("sv4cpio", "application/x-sv4cpio");
277 		MIME_TYPE_MAPPINGS.put("sv4crc", "application/x-sv4crc");
278 		MIME_TYPE_MAPPINGS.put("swf", "application/x-shockwave-flash");
279 		MIME_TYPE_MAPPINGS.put("t", "application/x-troff");
280 		MIME_TYPE_MAPPINGS.put("tar", "application/x-tar");
281 		MIME_TYPE_MAPPINGS.put("tcl", "application/x-tcl");
282 		MIME_TYPE_MAPPINGS.put("tex", "application/x-tex");
283 		MIME_TYPE_MAPPINGS.put("texi", "application/x-texinfo");
284 		MIME_TYPE_MAPPINGS.put("texinfo", "application/x-texinfo");
285 		MIME_TYPE_MAPPINGS.put("tif", "image/tiff");
286 		MIME_TYPE_MAPPINGS.put("tiff", "image/tiff");
287 		MIME_TYPE_MAPPINGS.put("tr", "application/x-troff");
288 		MIME_TYPE_MAPPINGS.put("tsv", "text/tab-separated-values");
289 		MIME_TYPE_MAPPINGS.put("txt", "text/plain");
290 		MIME_TYPE_MAPPINGS.put("ulw", "audio/basic");
291 		MIME_TYPE_MAPPINGS.put("ustar", "application/x-ustar");
292 		MIME_TYPE_MAPPINGS.put("xbm", "image/x-xbitmap");
293 		MIME_TYPE_MAPPINGS.put("xml", "text/xml");
294 		MIME_TYPE_MAPPINGS.put("xpm", "image/x-xpixmap");
295 		MIME_TYPE_MAPPINGS.put("xsl", "text/xml");
296 		MIME_TYPE_MAPPINGS.put("xsd", "text/xml");
297 		MIME_TYPE_MAPPINGS.put("rng", "text/xml");
298 		MIME_TYPE_MAPPINGS.put("xwd", "image/x-xwindowdump");
299 		MIME_TYPE_MAPPINGS.put("wav", "audio/x-wav");
300 		MIME_TYPE_MAPPINGS.put("svg", "image/svg+xml");
301 		MIME_TYPE_MAPPINGS.put("svgz", "image/svg+xml");
302 		MIME_TYPE_MAPPINGS.put("wbmp", "image/vnd.wap.wbmp");
303 		MIME_TYPE_MAPPINGS.put("wml", "text/vnd.wap.wml");
304 		MIME_TYPE_MAPPINGS.put("wmlc", "application/vnd.wap.wmlc");
305 		MIME_TYPE_MAPPINGS.put("wmls", "text/vnd.wap.wmlscript");
306 		MIME_TYPE_MAPPINGS.put("wmlscriptc", "application/vnd.wap.wmlscriptc");
307 		MIME_TYPE_MAPPINGS.put("wrl", "x-world/x-vrml");
308 		MIME_TYPE_MAPPINGS.put("Z", "application/x-compress");
309 		MIME_TYPE_MAPPINGS.put("z", "application/x-compress");
310 		MIME_TYPE_MAPPINGS.put("zip", "application/zip");
311 		MIME_TYPE_MAPPINGS = Collections.unmodifiableMap(MIME_TYPE_MAPPINGS);
312 	}
313 
314 	/***
315 	 * The Javascript resources cache, used for binrary resources or when client
316 	 * does not support a GZIPed response
317 	 */
318 	protected static Map<String, byte[]> cache = new ConcurrentHashMap<String, byte[]>();
319 
320 	/***
321 	 * The Javascript GZIPed resources cache, used for non-binary resources when
322 	 * client supports it
323 	 */
324 	protected static Map<String, byte[]> gzipedCache = new ConcurrentHashMap<String, byte[]>();
325 
326 	/***
327 	 * <code>init-param</code> accepting a whitespace-seperated list of
328 	 * allowed file extentions. The default value is js png jpg gif txt html htm
329 	 * xml xsl xslt svg svgz swf initialization parameter.
330 	 */
331 	protected Set<String> allowedExtentions;
332 
333 	/***
334 	 * <code>init-param</code> for the filter's base mapping path.
335 	 * <strong>Must</strong> be the same as the <code>url-pattern</code>
336 	 * mapping for the filter, e.g. /lib/js
337 	 */
338 	protected String basePath = "/lib/js";
339 
340 	/***
341 	 * <code>init-param</code> to control whether to send an HTTP 404 ("not
342 	 * found") code or let go of the filter chain to the application in case the
343 	 * requested resource cannot be found. Possible values are {true, false} The
344 	 * default behaviour is to send a 404 (true)
345 	 */
346 	protected boolean send404 = true;
347 
348 	/***
349 	 * <code>init-param</code> to control the value used for the Cache-Control
350 	 * HTTP header. Default is max-age=86400
351 	 */
352 	private String cacheControl = "max-age=86400";
353 
354 	/***
355 	 * <code>init-param</code> to control whether to send textual GZIPed
356 	 * textual content to increase HTTP performance. Possible values are {true,
357 	 * false} The default true
358 	 */
359 	protected boolean enableGzip = true;
360 
361 	/***
362 	 * <code>init-param</code> to control whether GZIPed resources are cached
363 	 * to avoid compression overhead every time they are requested. Default is true.
364 	 */
365 	private boolean enableGzipCache = true;
366 
367 	/***
368 	 * <code>init-param</code> to control whether resources are cached to
369 	 * avoid I/O overhead every time they are requested. Default is true.
370 	 */
371 	private boolean enableCache = true;
372 
373 	/***
374 	 * Get the MIME type for the given file extention
375 	 * 
376 	 * @param extention
377 	 *            The file extention
378 	 * @return The MIME type for the file extention if permitted/known possible.
379 	 */
380 	protected String getMimeTypeFromFileExtention(String extention) {
381 		String mimeType = null;
382 		if (extention != null) {
383 			mimeType = MIME_TYPE_MAPPINGS.get(extention);
384 		}
385 		return mimeType;
386 	}
387 
388 	/***
389 	 * Get the file extention of the given filename or path.
390 	 * 
391 	 * @param resourcePath
392 	 * @return the extention or <code>null</code>
393 	 */
394 	private String getFileExtention(String resourcePath) {
395 		String extention = null;
396 		if (resourcePath != null) {
397 			int extensionIndex = resourcePath.lastIndexOf('.');
398 			if (extensionIndex != -1
399 					&& extensionIndex + 1 < resourcePath.length()) {
400 				extention = resourcePath.substring(extensionIndex + 1);
401 			}
402 		}
403 		return extention;
404 	}
405 
406 	/***
407 	 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
408 	 *      javax.servlet.ServletResponse, javax.servlet.FilterChain)
409 	 */
410 	public void doFilter(ServletRequest request, ServletResponse response,
411 			FilterChain chain) throws IOException, ServletException {
412 		HttpServletRequest req = (HttpServletRequest) request;
413 		HttpServletResponse res = (HttpServletResponse) response;
414 		// only HTTP GET and HEAD for now, sorry :-)
415 		String httpMethod = req.getMethod().toUpperCase();
416 		if (!(httpMethod.equalsIgnoreCase("GET") || httpMethod.equalsIgnoreCase("HEAD"))) {
417 			// send a Non implemented
418             if(log.isDebugEnabled()){
419                 log.debug("Sent 501: Non Implemented for method: "+httpMethod+", on resource:"+req.getRequestURI() + ", remote host: " +req.getRemoteHost());
420             }
421 			res.sendError(501);
422 		} else {
423 			String requestURI = req.getRequestURI();
424 			String resourcePath = this.getResourcePath(requestURI);
425 			String fileExtention = this.getFileExtention(resourcePath);
426 			String mimeType = this.getMimeTypeFromFileExtention(fileExtention);
427 			boolean isText = this.getMimeTypeFromFileExtention(fileExtention)
428 					.startsWith("text/");
429 			boolean sendGziped = false;
430 			String ae = req.getHeader("accept-encoding");
431 			if (this.enableGzip && isText && ae != null && ae.indexOf("gzip") != -1) {
432 				sendGziped = true;
433 			}
434 			// is the resource allowed?
435 			if (this.allowedExtentions.contains(fileExtention.toLowerCase())
436 					&& resourcePath.indexOf("META-INF") == -1) {
437 				byte[] resourceData = null;
438 				if (sendGziped) {
439 					resourceData = this.getResourceGziped(resourcePath);
440 				}
441 				else{
442 					resourceData = this.getResource(resourcePath);
443 				}
444 				// write resource in response
445 				if (resourceData != null) {
446 					res.setHeader("Cache-Control", this.cacheControl);
447 					if (mimeType != null) {
448 						res.setContentType(mimeType);
449 					}
450 					if(sendGziped){
451 						res.addHeader("Content-Encoding", "gzip");
452 					}
453 					res.setContentLength(resourceData.length);
454 					ServletOutputStream out = response.getOutputStream();
455 					// only write a response body for HTTP GET
456 					if (httpMethod.equalsIgnoreCase("GET")) {
457                         if(log.isDebugEnabled()){
458                             log.debug("Writing resource to response stream, size: "+resourceData.length);
459                         }
460 						out.write(resourceData);
461 					}
462 					out.flush();
463 					out.close();
464 				} else {
465 					if (this.send404) {
466                         log.warn("Sending 404: Not found, for method: "+httpMethod+", on resource:"+req.getRequestURI() + ", remote host: " +req.getRemoteHost());
467 						res.sendError(404);
468 					} else {
469 						chain.doFilter(request, response);
470 					}
471 				}
472 			} else {
473 				// Not allowed, send a 403 Forbidden:
474                 log.warn("Sending 501: Forbidden for method: "+httpMethod+", on resource with disallowed extention:"+req.getRequestURI() + ", remote host: " +req.getRemoteHost());
475 				((HttpServletResponse) response).sendError(403);
476 			}
477 		}
478 	}
479 
480 	/***
481 	 * Obtain the requsted resource bytes if a match is found in the classpath
482 	 * or <code>null</code> otherwise. This method will try to use the caches
483 	 * or update them if enabled.
484 	 * 
485 	 * @param resourcePath
486 	 *            the path to match in the classpath
487 	 * @return the requsted resource bytes or <code>null</code> if no match is
488 	 *         found.
489 	 * @throws IOException
490 	 */
491 	private byte[] getResource(String resourcePath) throws IOException {
492 		byte[] resource = null;
493 		// load from gzipedCache if enabled and available
494 		if (this.enableCache && cache.containsKey(resourcePath)) {
495 			resource = cache.get(resourcePath);
496             if(log.isDebugEnabled()){
497                 log.debug("Resource "+resourcePath+" was loaded from the non-GZIPed cache, size: "+resource.length);
498             }
499 		} else {
500 			// load from classpath
501 			resource = ClasspathResourceUtils
502 					.getResourceBytesOrNull(resourcePath);
503             if(log.isDebugEnabled()){
504                 log.debug("Resource "+resourcePath+" was loaded from the classpath, size: "+resource.length);
505             }
506 			// add a clone to the gzipped cache if enabled
507 			if (this.enableCache) {
508 				cache.put(resourcePath, resource.clone());
509                 if(log.isDebugEnabled()){
510                     log.debug("Resource "+resourcePath+" was added to (non-GZIPed) cache, size: "+resource.length);
511                 }
512 			}
513 			// add to normal cache if enabled
514 			if (this.enableGzipCache) {
515 				gzipedCache.put(resourcePath, resource != null ? this
516 						.toGzipedBytes(resource.clone()) : resource);
517                 if(log.isDebugEnabled()){
518                     log.debug("Resource "+resourcePath+" was added to the GZIP cache");
519                 }
520 			}
521 		}
522 		return resource;
523 	}
524 
525 	/***
526 	 * Obtain the GZIPed resource bytes if a match is found in the classpath or
527 	 * <code>null</code> otherwise. This method will try to use the caches or
528 	 * update them if enabled.
529 	 * 
530 	 * @param resourcePath
531 	 *            the path to match in the classpath
532 	 * @return the requsted resource bytes or <code>null</code> if no match is
533 	 *         found.
534 	 * @throws IOException
535 	 */
536 	private byte[] getResourceGziped(String resourcePath) throws IOException{
537 			byte[] resource = null;
538 			// load from gzipedCache if enabled and available
539 			if(this.enableGzipCache && gzipedCache.containsKey(resourcePath)){
540 				resource = gzipedCache.get(resourcePath);
541                 if(log.isDebugEnabled()){
542                     log.debug("Resource "+resourcePath+" was loaded from the GZIP cache, size: "+resource.length);
543                 }
544 			}
545 			else{
546 				// load from normal cache if enabled and available
547 				if(this.enableCache && cache.containsKey(resourcePath)){
548 					resource = this.toGzipedBytes(cache.get(resourcePath));
549 				}
550 				else{
551 					// load from classpath
552 					resource = ClasspathResourceUtils.getResourceBytesOrNull(resourcePath);
553                     if(log.isDebugEnabled()){
554                         log.debug("Resource "+resourcePath+" was loaded from the classpath, size: "+resource.length);
555                     }
556 					// add a clone to un-gzipped cache if enabled
557 					if(this.enableCache){
558 						 cache.put(resourcePath, resource.clone());
559                          if(log.isDebugEnabled()){
560                                 log.debug("Resource "+resourcePath+" was added to (non-GZIPed) cache, size: "+resource.length);
561                             }
562 					}
563 					
564 					// gzip
565 					resource = this.toGzipedBytes(resource);
566                     if(log.isDebugEnabled()){
567                         log.debug("Resource "+resourcePath+" was GZIPed, size: "+resource.length);
568                     }
569 				}
570 				// add to gzipedCache if enabled
571 				if(this.enableGzipCache){
572 					gzipedCache.put(resourcePath, resource);
573                     if(log.isDebugEnabled()){
574                         log.debug("Resource "+resourcePath+" was added to the GZIP cache, size: "+resource.length);
575                     }
576 				}
577 			}
578 			return resource;
579 		}
580 
581 	/***
582 	 * Construct the resource path based on the request information
583 	 * 
584 	 * @param request
585 	 * @return
586 	 * @throws ServletException
587 	 */
588 	private String getResourcePath(String requestURI) throws ServletException {
589 		int basePathIndex = requestURI.indexOf(this.basePath);
590 		if (basePathIndex == -1) {
591 			throw new ServletException(
592 					"The value of the basePath init-parameter is incompatible with the filter mapping");
593 		}
594 		String resPath = requestURI.substring(basePathIndex
595 				+ this.basePath.length());
596 		// we need to look for "package/name/resource"
597 		// instead of "/package/name/resource"
598 		if (resPath.startsWith("/")) {
599 			resPath = resPath.substring(1);
600 		}
601 		log.info("Resource path: " + resPath);
602 		return resPath;
603 	}
604 
605 	/***
606 	 * Get a GZIPed version of the given data (those are kept intact)
607 	 * 
608 	 * @param data
609 	 *            the given data to GZIP
610 	 * @return a GZIPed version of the given data or <code>null</code> if the
611 	 *         given data are also <code>null</code> or empty
612 	 * @throws IOException
613 	 */
614 	private byte[] toGzipedBytes(byte[] data) throws IOException {
615 		byte[] results = null;
616 		if (data != null) {
617 			ByteArrayOutputStream baos = new ByteArrayOutputStream();
618 			GZIPOutputStream out = new GZIPOutputStream(baos);
619 			out.write(data, 0, data.length);
620 			out.finish();
621 			out.close();
622 			results = baos.toByteArray();
623 			if (log.isDebugEnabled()) {
624 				log.debug("Compressed from " + data.length + " to "
625 						+ results.length + " bytes");
626 			}
627 		}
628 		return results;
629 	}
630 
631 	/***
632 	 * Initialize and configure based on any init parameters available
633 	 * 
634 	 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
635 	 */
636 	public void init(FilterConfig filterConfig) throws ServletException {
637 		// configure cache control
638 		String _cacheControl = filterConfig.getInitParameter("cacheControl");
639 		if (_cacheControl != null) {
640 			this.cacheControl = _cacheControl;
641 		}
642 		// configure gzip
643 		String _enableGzip = filterConfig.getInitParameter("enableGzip");
644 		if (_enableGzip != null) {
645 			this.enableGzip = BooleanUtils.toBoolean(_enableGzip);
646 		}
647 		// configure gzip cache
648 		String _enableGzipCache = filterConfig
649 				.getInitParameter("enableGzipCache");
650 		if (_enableGzipCache != null) {
651 			this.enableGzipCache = BooleanUtils.toBoolean(_enableGzipCache);
652 		}
653 		// configure cache
654 		String _enableCache = filterConfig.getInitParameter("enableCache");
655 		if (_enableCache != null) {
656 			this.enableCache = BooleanUtils.toBoolean(_enableCache);
657 		}
658 		// configure 404 flag
659 		String _send404 = filterConfig.getInitParameter("send404");
660 		if (_send404 != null) {
661 			this.send404 = BooleanUtils.toBoolean(_send404);
662 		}
663 		// configure base path
664 		String _basePath = filterConfig.getInitParameter("basePath");
665 		if (_basePath != null) {
666 			this.basePath = _basePath;
667 		}
668 		while (this.basePath.endsWith("/")) {
669 			// remove any trailing slashes
670 			this.basePath = this.basePath.substring(0,
671 					this.basePath.length() - 1);
672 		}
673 		if (this.basePath == null || this.basePath.length() == 0) {
674 			throw new ServletException(
675 					"Invalid value for init-param basePath: " + _basePath);
676 		}
677 		this.basePath = _basePath;
678 		// configure allowed file extentions
679 		String allowedExt = filterConfig.getInitParameter("allowedExtentions");
680 		if (allowedExt == null) {
681 			allowedExt = "js png jpg gif txt html htm xml xsl xslt svg svgz swf";
682 		}
683 		String[] allowedArray = allowedExt.replaceAll("//s{2,}", " ").trim()
684 				.split(" ");
685 		this.allowedExtentions = new HashSet<String>();
686 		for (int i = 0; i < allowedArray.length; i++) {
687 			this.allowedExtentions.add(allowedArray[i].toLowerCase());
688 		}
689 		log.info("Configured JavascriptDependencyFilter with send404: "
690 				+ this.send404 + ", basePath: " + this.basePath
691 				+ ", cacheControl: " + this.cacheControl + ", enableGzip: "
692 				+ this.enableGzip + ", enableGzipCache: "
693 				+ this.enableGzipCache + ", enableCache: " + this.enableCache
694 				+ ", allowedExtentions: " + allowedExt);
695 	}
696 
697 	/***
698 	 * Clean up by doing nothing :-)
699 	 * 
700 	 * @see javax.servlet.Filter#destroy()
701 	 */
702 	public void destroy() {
703 		// do nothing
704 	}
705 }