5fee73674e2240348fb0d2760e461f84a0966dfe
[percentcodec] / cachecontrol / src / main / java / de / halbekunst / juplo / cachecontrol / AcceleratorFilter.java
1 package de.halbekunst.juplo.cachecontrol;
2
3 import java.io.ByteArrayOutputStream;
4 import java.io.IOException;
5 import java.io.OutputStream;
6 import java.io.PrintWriter;
7 import java.text.ParseException;
8 import java.text.SimpleDateFormat;
9 import java.util.Collections;
10 import java.util.Enumeration;
11 import java.util.HashMap;
12 import java.util.Locale;
13 import java.util.Map;
14 import java.util.zip.GZIPOutputStream;
15 import javax.servlet.Filter;
16 import javax.servlet.FilterChain;
17 import javax.servlet.FilterConfig;
18 import javax.servlet.ServletException;
19 import javax.servlet.ServletOutputStream;
20 import javax.servlet.ServletRequest;
21 import javax.servlet.ServletResponse;
22 import javax.servlet.http.HttpServletRequest;
23 import javax.servlet.http.HttpServletResponse;
24 import javax.servlet.http.HttpServletResponseWrapper;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27 import org.springframework.beans.factory.annotation.Autowire;
28 import org.springframework.beans.factory.annotation.Autowired;
29 import org.springframework.beans.factory.annotation.Configurable;
30
31
32
33 /**
34  *
35  * @author kai
36  */
37 @Configurable(autowire=Autowire.BY_NAME)
38 public class AcceleratorFilter implements Filter {
39   private final static Logger log = LoggerFactory.getLogger(AcceleratorFilter.class);
40
41   private final static Map<String,String> EMPTY = Collections.unmodifiableMap(new HashMap<String,String>());
42
43   public final static String REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
44   public final static String RESPONSE_WRAPPER = AcceleratorFilter.class.getName() + ".RESPONSE_WRAPPER";
45
46
47   @Autowired CacheControl cacheControl;
48   @Autowired(required=true) Integer buffer;
49   @Autowired(required=true) String eTag;
50   @Autowired(required=true) Boolean weak;
51   @Autowired(required=true) Long lastModified;
52   @Autowired(required=true) Integer cacheSeconds;
53
54
55   @Override
56   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
57     if (!(request instanceof HttpServletRequest)) {
58       log.error("AcceleratorFilter can only handle HTTP-requests");
59       chain.doFilter(request, response);
60       return;
61     }
62
63     /** Prüfen, ob es sich um eine Anfrage für einen JSP-Include handelt */
64     if (request.getAttribute(REQUEST_URI_ATTRIBUTE) != null) {
65       log.debug("Includes cannot be accelerated");
66       chain.doFilter(request, response);
67       return;
68     }
69
70     HttpServletRequest httpRequest = (HttpServletRequest)request;
71     HttpServletResponse httpResponse = (HttpServletResponse)response;
72     AccelerationWrapper wrapper = new AccelerationWrapper(httpRequest, httpResponse);
73     httpRequest.setAttribute(RESPONSE_WRAPPER, wrapper);
74     cacheControl.init(wrapper);
75     chain.doFilter(request, wrapper);
76     wrapper.finish();
77   }
78
79   @Override
80   public void init(FilterConfig filterConfig) throws ServletException {
81   }
82
83   @Override
84   public void destroy() {
85   }
86
87
88   class AccelerationWrapper extends HttpServletResponseWrapper implements CacheMethodHandle {
89
90     private final HttpServletRequest request;
91     private final HttpServletResponse response;
92
93     boolean zipped; // CacheControll greift direkt auf dieses Flag zu!
94     boolean forceCompression = false;
95
96     private CountingServletOutputStream out;
97     private ServletOutputStream stream;
98     private PrintWriter writer;
99
100     private boolean guessing = true;
101
102     private long now;
103     private int status;
104     private int cacheSeconds;
105     private boolean cacheSecondsSet = false;
106     private long lastModified, expires = 0l;
107     private String eTag;
108     private boolean weak;
109     private Map<String,String> cacheParams;
110
111
112     AccelerationWrapper(HttpServletRequest request, HttpServletResponse response) throws IOException {
113       super(response);
114
115       this.request = request;
116       this.response = response;
117
118       now = System.currentTimeMillis();
119       status = HttpServletResponse.SC_OK;
120       cacheSeconds = AcceleratorFilter.this.cacheSeconds;
121       lastModified = AcceleratorFilter.this.lastModified;
122       eTag = AcceleratorFilter.this.eTag;
123       weak = AcceleratorFilter.this.weak;
124       cacheParams = new HashMap<String,String>();
125
126       zipped = false;
127       Enumeration values = request.getHeaders(Headers.HEADER_ACCEPT_ENCODING);
128       while (values.hasMoreElements()) {
129         String value = (String) values.nextElement();
130         if (value.indexOf("gzip") != -1) {
131           zipped = true;
132           break;
133         }
134       }
135       if (zipped)
136         out = new GZIPServletOutputStream();
137       else
138         out = new CountingServletOutputStream();
139     }
140
141     public void finish() throws IOException {
142       out.close();
143     }
144
145
146     @Override
147     public void setStatus(int sc) {
148       response.setStatus(sc);
149       status = sc;
150       try {
151         cacheControl.decorate(request, response, this);
152       }
153       catch (Exception e) {
154         log.error("Error while decorating response", e);
155       }
156     }
157
158     @Override
159     public void setStatus(int sc, String sm) {
160       response.setStatus(sc,sm);
161       status = sc;
162       try {
163         cacheControl.decorate(request, response, this);
164       }
165       catch (Exception e) {
166         log.error("Error while decorating response", e);
167       }
168     }
169
170     @Override
171     public void addDateHeader(String name, long value) {
172
173       if (!guessing) {
174         super.addDateHeader(name, value);
175         return;
176       }
177
178       if (Headers.HEADER_DATE.equalsIgnoreCase(name)) {
179         now = value;
180         calculateCacheSeconds();
181         return;
182       }
183
184       if (Headers.HEADER_EXPIRES.equalsIgnoreCase(name)) {
185         expires = value;
186         calculateCacheSeconds();
187         return;
188       }
189
190       if (Headers.HEADER_LAST_MODIFIED.equalsIgnoreCase(name)) {
191         lastModified = value;
192         return;
193       }
194
195       /** Unknown header: pass throug! */
196       super.addDateHeader(name, value);
197     }
198
199     @Override
200     public void addHeader(String name, String value) {
201
202       if (!guessing) {
203         super.addHeader(name, value);
204         return;
205       }
206
207       if (value == null)
208         return;
209       analyzeHeader(name, value, false);
210     }
211
212     @Override
213     public void addIntHeader(String name, int value) {
214
215       if (!guessing) {
216         super.addIntHeader(name, value);
217         return;
218       }
219
220       analyzeHeader(name, Integer.toString(value), false);
221     }
222
223     @Override
224     public void setDateHeader(String name, long value) {
225
226       if (!guessing) {
227         super.setDateHeader(name, value);
228         return;
229       }
230
231       if (Headers.HEADER_DATE.equalsIgnoreCase(name)) {
232         now = value;
233         calculateCacheSeconds();
234         return;
235       }
236
237       if (Headers.HEADER_EXPIRES.equalsIgnoreCase(name)) {
238         expires = value;
239         calculateCacheSeconds();
240         return;
241       }
242
243       if (Headers.HEADER_LAST_MODIFIED.equalsIgnoreCase(name)) {
244         lastModified = value;
245         return;
246       }
247
248       /** Unknown header: pass throug! */
249       super.setDateHeader(name, value);
250     }
251
252     @Override
253     public void setHeader(String name, String value) {
254
255       if (!guessing) {
256         super.setHeader(name, value);
257         return;
258       }
259
260       analyzeHeader(name, value, true);
261     }
262
263     @Override
264     public void setIntHeader(String name, int value) {
265
266       if (!guessing) {
267         super.setIntHeader(name, value);
268         return;
269       }
270
271       analyzeHeader(name, Integer.toString(value), true);
272     }
273
274     @Override
275     public ServletOutputStream getOutputStream() throws IOException {
276
277       if (writer != null)
278         throw new IllegalStateException("ServletOutputStream and PrintWriter cannot be requested both!");
279
280       if (stream == null) {
281         stream = out;
282       }
283
284       return out;
285     }
286
287     @Override
288     public PrintWriter getWriter() throws IOException {
289
290       if (stream != null)
291         throw new IllegalStateException("ServletOutputStream and PrintWriter cannot be requested both!");
292
293       if (writer == null) {
294         writer = new PrintWriter(out);
295       }
296
297       return writer;
298     }
299
300     @Override
301     public void setContentLength(int len) {
302       if (zipped)
303         log.info("Supressing explicit content-length {} for request {}, because content will be zipped!", len, request.getRequestURI());
304       else
305         response.setContentLength(len);
306     }
307
308     @Override
309     public void setBufferSize(int size) {
310
311       out.setBuffer(size);
312       response.setBufferSize(size);
313     }
314
315     @Override
316     public void flushBuffer() throws IOException {
317
318       forceCompression = true;
319       cacheControl.decorate(request, response, this);
320       response.flushBuffer();
321     }
322
323     @Override
324     public void resetBuffer() {
325
326       response.resetBuffer();
327       try {
328         if (zipped)
329           out = new GZIPServletOutputStream();
330         else
331           out = new CountingServletOutputStream();
332       }
333       catch (IOException e) {
334         throw new IllegalStateException(e);
335       }
336       stream = null;
337       writer = null;
338     }
339
340     @Override
341     public void reset() {
342
343       response.reset();
344       try {
345         if (zipped)
346           out = new GZIPServletOutputStream();
347         else
348           out = new CountingServletOutputStream();
349       }
350       catch (IOException e) {
351         throw new IllegalStateException(e);
352       }
353       stream = null;
354       writer = null;
355
356       /** Cookies has been cleared! Reinitialize decorator... */
357       forceCompression = false;
358       cacheControl.init(this);
359     }
360
361
362
363     @Override
364     public boolean isZipped() {
365       return out.isZipped();
366     }
367
368     @Override
369     public long getTimestamp() {
370       return now;
371     }
372
373     @Override
374     public int accepts(HttpServletRequest request) {
375       return status;
376     }
377
378     @Override
379     public int getCacheSeconds(HttpServletRequest request) {
380       return cacheSeconds;
381     }
382
383     @Override
384     public long getLastModified(HttpServletRequest request) {
385       return lastModified;
386     }
387
388     @Override
389     public String getETag(HttpServletRequest request) {
390       return eTag;
391     }
392
393     @Override
394     public boolean isETagWeak() {
395       return weak;
396     }
397
398     @Override
399     public void cacheControl(HttpServletRequest request, Map<String, String> cacheControlMap) {
400       cacheControlMap.putAll(cacheParams);
401     }
402
403     @Override
404     public Map<String,String> getAdditionalHeaders(HttpServletRequest request) {
405       return EMPTY;
406     }
407
408     public void supressGuessing() {
409       guessing = false;
410     }
411
412
413     private void analyzeHeader(String name, String value, boolean overwrite) {
414       if (name == null)
415         return;
416       name = name.trim();
417
418       if (name.equalsIgnoreCase(Headers.HEADER_DATE)) {
419         if (value == null) {
420           if (overwrite) {
421             now = System.currentTimeMillis();
422             cacheSeconds = AcceleratorFilter.this.cacheSeconds;
423           }
424           return;
425         }
426         try {
427           SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
428           now = parser.parse(value).getTime();
429           calculateCacheSeconds();
430         }
431         catch (ParseException e) {
432           log.warn("ignoring date for header \"Date\" in invalid format: {}", value);
433         }
434         return;
435       }
436
437       if (name.equalsIgnoreCase(Headers.HEADER_EXPIRES)) {
438         if (value == null) {
439           if (overwrite) {
440             expires = 0;
441             cacheSeconds = AcceleratorFilter.this.cacheSeconds;
442           }
443           return;
444         }
445         try {
446           SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
447           expires = parser.parse(value).getTime();
448           calculateCacheSeconds();
449         }
450         catch (ParseException e) {
451           log.warn("ignoring date for header \"Expires\" in invalid format: {}", value);
452         }
453         return;
454       }
455
456       if (name.equalsIgnoreCase(Headers.HEADER_LAST_MODIFIED)) {
457         if (value == null) {
458           if (overwrite)
459             lastModified = AcceleratorFilter.this.lastModified;
460           return;
461         }
462         try {
463           SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
464           lastModified = parser.parse(value).getTime();
465         }
466         catch (ParseException e) {
467           log.warn("ignoring date for header \"Last-Modified\" in invalid format: {}", value);
468         }
469         return;
470       }
471
472       if (name.equalsIgnoreCase(Headers.HEADER_ETAG)) {
473         if (value == null) {
474           if (overwrite) {
475             eTag = AcceleratorFilter.this.eTag;
476             weak = AcceleratorFilter.this.weak;
477           }
478           return;
479         }
480         value = value.trim();
481         int start = 0;
482         int end = value.length();
483         if (value.startsWith("W/")) {
484           weak = true;
485           start = 2;
486         }
487         else {
488           weak = false;
489         }
490         if (value.charAt(start) == '"')
491           start++;
492         else
493           log.warn("Quote at the beginning ov ETag is missing: {}", value);
494         if (value.charAt(end -1) == '"')
495           end--;
496         else
497           log.warn("Quote at the end of ETag is missing: {}", value);
498         eTag = value.substring(start, end);
499         String filtered = eTag.replaceAll("[^\\x00-\\x21\\x23-\\x7F]+","");
500         if (filtered.length() < eTag.length()) {
501           log.warn("filtering out illegal characters in ETag: \"{}\" -> \"{}\"", eTag, filtered);
502           eTag = filtered;
503         }
504       }
505
506       if (name.equalsIgnoreCase(Headers.HEADER_CACHE_CONTROL)) {
507         if (overwrite)
508           cacheParams.clear();
509         if (value == null)
510           return;
511         for (String param : value.split(",")) {
512           param = param.trim();
513           int pos = param.indexOf("=");
514           if (pos < 0) {
515             cacheParams.put(param, null);
516           }
517           else {
518             String paramName = param.substring(0, pos).trim();
519             if (paramName.equalsIgnoreCase("max-age")) {
520               try {
521                 cacheSeconds = Integer.parseInt(param.substring(pos + 1));
522                 cacheSecondsSet = true;
523               }
524               catch (NumberFormatException e) {
525                 log.warn("illegal value for Header \"Cache-Control\":", param);
526               }
527             }
528             else {
529               cacheParams.put(paramName, param.substring(pos + 1));
530             }
531           }
532         }
533         return;
534       }
535
536       if (name.equalsIgnoreCase(Headers.HEADER_PRAGMA)) {
537         if (value != null && value.trim().equalsIgnoreCase("no-cache"))
538           cacheSeconds = 0;
539         return;
540       }
541
542       /** Pass header through, if no value from intrest was found */
543       if (overwrite)
544         super.setHeader(name, value);
545       else
546         super.addHeader(name, value);
547     }
548
549     private void calculateCacheSeconds() {
550       if (!cacheSecondsSet && expires >= now) {
551         cacheSeconds = (int)(expires/1000 - now/1000);
552         log.debug("calculating cache-seconds from DATE and EXPIRES: {}", cacheSeconds);
553       }
554     }
555
556
557     class CountingServletOutputStream extends ServletOutputStream {
558
559       private OutputStream out;
560       int left;
561       boolean empty;
562
563
564       CountingServletOutputStream() throws IOException {
565         out = response.getOutputStream();
566         left = buffer;
567         empty = true;
568       }
569
570
571       void setBuffer(int size) throws IllegalStateException {}
572
573       boolean isZipped() {
574         return false;
575       }
576
577       void finish() throws IOException {
578         decorate();
579       }
580
581       void decorate() throws IOException {
582         try {
583           AcceleratorFilter.this.cacheControl.decorate(AccelerationWrapper.this.request, response, AccelerationWrapper.this);
584         }
585         catch (Exception e) {
586           log.error("Error while guessing Cache-Header's", e);
587           response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
588         }
589       }
590
591
592       @Override
593       public void close() throws IOException {
594         decorate();
595         out.close();
596       }
597
598       @Override
599       public void flush() throws IOException {
600         decorate();
601         out.flush();
602       }
603
604       @Override
605       public void write(int i) throws IOException {
606         empty = false;
607         if (left == 0)
608           decorate();
609         left--;
610         out.write(i);
611       }
612     }
613
614
615     final class GZIPServletOutputStream extends CountingServletOutputStream {
616
617       final static int MINIMAL_BUFFER_SIZE = 128;
618
619       private ByteArrayOutputStream buffer;
620       private OutputStream out;
621       private GZIPOutputStream zout;
622       private int bufferSize;
623       private boolean decorated = false;
624
625
626       public GZIPServletOutputStream() throws IOException {
627         setBuffer(AcceleratorFilter.this.buffer);
628       }
629
630
631       @Override
632       void finish() throws IOException {
633         decorate();
634         zout.finish();
635         super.out.write(buffer.toByteArray());
636       }
637
638       @Override
639       boolean isZipped() {
640         if (decorated)
641           return true;
642         return !empty;
643       }
644
645       @Override
646       void setBuffer(int size) throws IllegalStateException {
647         if (!empty)
648           throw new IllegalStateException("attemp to change buffer size after writing data to response!");
649
650         if (size > MINIMAL_BUFFER_SIZE) {
651           left = size;
652           try {
653             bufferSize = size;
654             out = response.getOutputStream();
655             buffer = new ByteArrayOutputStream(size);
656             zout = new GZIPOutputStream(buffer, size);
657             super.out = zout;
658           }
659           catch (IOException e) {
660             throw new IllegalStateException(e);
661           }
662         }
663       }
664
665
666
667       @Override
668       public void close() throws IOException {
669         decorate();
670         zout.close();
671         out.write(buffer.toByteArray());
672         out.close();
673       }
674
675       @Override
676       public void flush() throws IOException {
677         decorate();
678         out.write(buffer.toByteArray());
679         out.flush();
680       }
681
682       @Override
683       public void write(int i) throws IOException {
684         empty = false;
685         if (left == 0) {
686           if (!decorated) {
687             decorate();
688             decorated = true;
689           }
690           out.write(buffer.toByteArray());
691           buffer.reset();
692           left = bufferSize;
693         }
694         left--;
695         zout.write(i);
696       }
697     }
698   }
699 }