1
2
3
4
5
6
7
8
9
10
11
12
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 * <filter>
59 * <filter-name>JavascriptDependencyFilter</filter-name>
60 * <filter-class>gr.abiss.mvn.plugins.jstools.web.JavascriptDependencyFilter</filter-class>
61 * <init-param>
62 * <!--
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 * -->
68 * <param-name><strong>allowedExtentions</strong></param-name>
69 * <param-value>js png jpg gif txt html htm xml xsl xslt svg svgz swf</param-value>
70 * </init-param>
71 * <init-param>
72 * <!--
73 * Optional, used to configure the Cache-Control HTTP header.
74 * Default is "max-age=86400"
75 * -->
76 * <param-name><strong>cacheControl</strong></param-name>
77 * <param-value>max-age=86400</param-value>
78 * </init-param>
79 * <init-param>
80 * <!--
81 * Optional, enable gzip compression for browsers that support it
82 * (based on HTTP request headers). Default is true.
83 * -->
84 * <param-name><strong>enableGzip</strong></param-name>
85 * <param-value>true</param-value>
86 * </init-param>
87 * <init-param>
88 * <!--
89 * Optional, control whether resources are cached to
90 * avoid I/O overhead every time they are requested. Default is true.
91 * -->
92 * <param-name><strong>enableCache</strong></param-name>
93 * <param-value>true</param-value>
94 * </init-param>
95 * <init-param>
96 * <!--
97 * Optional, control whether gzipped resources are cached to
98 * avoid GZIPing overhead every time they are requested. Default is true.
99 * -->
100 * <param-name><strong>enableGzipedCache</strong></param-name>
101 * <param-value>true</param-value>
102 * </init-param>
103 * <init-param>
104 * <!--
105 * Optional, tells the filter whether to send an HTTP 404 ("not found") 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 * -->
110 * <param-name><strong>send404</strong></param-name>
111 * <param-value>true</param-value>
112 * </init-param>
113 * <init-param>
114 * <!--
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 * -->
120 * <param-name><strong>basePath</strong></param-name>
121 * <param-value>/lib/js/</param-value>
122 * </init-param>
123 * </filter>
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 * <filter-mapping>
133 * <filter-name>JavascriptDependencyFilter</filter-name>
134 * <!-- matches the default value of the <strong>basePath</strong>
135 * <code>
136 * init - param
137 * </code>
138 * <url-pattern>/lib/js/</url-pattern>
139 * </filter-mapping>
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 * <script type="text/javascript"
147 * src="/lib/js/gr/abiss/js/sarissa/sarissa.js">
148 * <script&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
415 String httpMethod = req.getMethod().toUpperCase();
416 if (!(httpMethod.equalsIgnoreCase("GET") || httpMethod.equalsIgnoreCase("HEAD"))) {
417
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
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
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
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
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
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
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
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
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
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
547 if(this.enableCache && cache.containsKey(resourcePath)){
548 resource = this.toGzipedBytes(cache.get(resourcePath));
549 }
550 else{
551
552 resource = ClasspathResourceUtils.getResourceBytesOrNull(resourcePath);
553 if(log.isDebugEnabled()){
554 log.debug("Resource "+resourcePath+" was loaded from the classpath, size: "+resource.length);
555 }
556
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
565 resource = this.toGzipedBytes(resource);
566 if(log.isDebugEnabled()){
567 log.debug("Resource "+resourcePath+" was GZIPed, size: "+resource.length);
568 }
569 }
570
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
597
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
638 String _cacheControl = filterConfig.getInitParameter("cacheControl");
639 if (_cacheControl != null) {
640 this.cacheControl = _cacheControl;
641 }
642
643 String _enableGzip = filterConfig.getInitParameter("enableGzip");
644 if (_enableGzip != null) {
645 this.enableGzip = BooleanUtils.toBoolean(_enableGzip);
646 }
647
648 String _enableGzipCache = filterConfig
649 .getInitParameter("enableGzipCache");
650 if (_enableGzipCache != null) {
651 this.enableGzipCache = BooleanUtils.toBoolean(_enableGzipCache);
652 }
653
654 String _enableCache = filterConfig.getInitParameter("enableCache");
655 if (_enableCache != null) {
656 this.enableCache = BooleanUtils.toBoolean(_enableCache);
657 }
658
659 String _send404 = filterConfig.getInitParameter("send404");
660 if (_send404 != null) {
661 this.send404 = BooleanUtils.toBoolean(_send404);
662 }
663
664 String _basePath = filterConfig.getInitParameter("basePath");
665 if (_basePath != null) {
666 this.basePath = _basePath;
667 }
668 while (this.basePath.endsWith("/")) {
669
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
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
704 }
705 }