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