AccelerationWrapper erweitert jetzt nicht mehr HttpServletResponseWrapper
[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.Cookie;
23 import javax.servlet.http.HttpServletRequest;
24 import javax.servlet.http.HttpServletResponse;
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 implements HttpServletResponse, 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
114       this.request = request;
115       this.response = response;
116
117       now = System.currentTimeMillis();
118       status = HttpServletResponse.SC_OK;
119       cacheSeconds = AcceleratorFilter.this.cacheSeconds;
120       lastModified = AcceleratorFilter.this.lastModified;
121       eTag = AcceleratorFilter.this.eTag;
122       weak = AcceleratorFilter.this.weak;
123       cacheParams = new HashMap<String,String>();
124
125       zipped = false;
126       Enumeration values = request.getHeaders(Headers.HEADER_ACCEPT_ENCODING);
127       while (values.hasMoreElements()) {
128         String value = (String) values.nextElement();
129         if (value.indexOf("gzip") != -1) {
130           zipped = true;
131           break;
132         }
133       }
134       if (zipped)
135         out = new GZIPServletOutputStream();
136       else
137         out = new CountingServletOutputStream();
138     }
139
140     public void finish() throws IOException {
141       out.close();
142     }
143
144
145     @Override
146     public void setStatus(int sc) {
147       response.setStatus(sc);
148       status = sc;
149       try {
150         cacheControl.decorate(request, response, this);
151       }
152       catch (Exception e) {
153         log.error("Error while decorating response", e);
154       }
155     }
156
157     @Override
158     public void setStatus(int sc, String sm) {
159       response.setStatus(sc,sm);
160       status = sc;
161       try {
162         cacheControl.decorate(request, response, this);
163       }
164       catch (Exception e) {
165         log.error("Error while decorating response", e);
166       }
167     }
168
169     @Override
170     public void addDateHeader(String name, long value) {
171
172       if (!guessing) {
173         response.addDateHeader(name, value);
174         return;
175       }
176
177       if (Headers.HEADER_DATE.equalsIgnoreCase(name)) {
178         now = value;
179         calculateCacheSeconds();
180         return;
181       }
182
183       if (Headers.HEADER_EXPIRES.equalsIgnoreCase(name)) {
184         expires = value;
185         calculateCacheSeconds();
186         return;
187       }
188
189       if (Headers.HEADER_LAST_MODIFIED.equalsIgnoreCase(name)) {
190         lastModified = value;
191         return;
192       }
193
194       /** Unknown header: pass throug! */
195       response.addDateHeader(name, value);
196     }
197
198     @Override
199     public void addHeader(String name, String value) {
200
201       if (!guessing) {
202         response.addHeader(name, value);
203         return;
204       }
205
206       if (value == null)
207         return;
208       analyzeHeader(name, value, false);
209     }
210
211     @Override
212     public void addIntHeader(String name, int value) {
213
214       if (!guessing) {
215         response.addIntHeader(name, value);
216         return;
217       }
218
219       analyzeHeader(name, Integer.toString(value), false);
220     }
221
222     @Override
223     public void setDateHeader(String name, long value) {
224
225       if (!guessing) {
226         response.setDateHeader(name, value);
227         return;
228       }
229
230       if (Headers.HEADER_DATE.equalsIgnoreCase(name)) {
231         now = value;
232         calculateCacheSeconds();
233         return;
234       }
235
236       if (Headers.HEADER_EXPIRES.equalsIgnoreCase(name)) {
237         expires = value;
238         calculateCacheSeconds();
239         return;
240       }
241
242       if (Headers.HEADER_LAST_MODIFIED.equalsIgnoreCase(name)) {
243         lastModified = value;
244         return;
245       }
246
247       /** Unknown header: pass throug! */
248       response.setDateHeader(name, value);
249     }
250
251     @Override
252     public void setHeader(String name, String value) {
253
254       if (!guessing) {
255         response.setHeader(name, value);
256         return;
257       }
258
259       analyzeHeader(name, value, true);
260     }
261
262     @Override
263     public void setIntHeader(String name, int value) {
264
265       if (!guessing) {
266         response.setIntHeader(name, value);
267         return;
268       }
269
270       analyzeHeader(name, Integer.toString(value), true);
271     }
272
273     @Override
274     public ServletOutputStream getOutputStream() throws IOException {
275
276       if (writer != null)
277         throw new IllegalStateException("ServletOutputStream and PrintWriter cannot be requested both!");
278
279       if (stream == null) {
280         stream = out;
281       }
282
283       return out;
284     }
285
286     @Override
287     public PrintWriter getWriter() throws IOException {
288
289       if (stream != null)
290         throw new IllegalStateException("ServletOutputStream and PrintWriter cannot be requested both!");
291
292       if (writer == null) {
293         writer = new PrintWriter(out);
294       }
295
296       return writer;
297     }
298
299     @Override
300     public void setContentLength(int len) {
301       if (zipped)
302         log.info("Supressing explicit content-length {} for request {}, because content will be zipped!", len, request.getRequestURI());
303       else
304         response.setContentLength(len);
305     }
306
307     @Override
308     public void setBufferSize(int size) {
309
310       out.setBuffer(size);
311       response.setBufferSize(size);
312     }
313
314     @Override
315     public void flushBuffer() throws IOException {
316
317       forceCompression = true;
318       cacheControl.decorate(request, response, this);
319       response.flushBuffer();
320     }
321
322     @Override
323     public void resetBuffer() {
324
325       response.resetBuffer();
326       try {
327         if (zipped)
328           out = new GZIPServletOutputStream();
329         else
330           out = new CountingServletOutputStream();
331       }
332       catch (IOException e) {
333         throw new IllegalStateException(e);
334       }
335       stream = null;
336       writer = null;
337     }
338
339     @Override
340     public void reset() {
341
342       response.reset();
343       try {
344         if (zipped)
345           out = new GZIPServletOutputStream();
346         else
347           out = new CountingServletOutputStream();
348       }
349       catch (IOException e) {
350         throw new IllegalStateException(e);
351       }
352       stream = null;
353       writer = null;
354
355       /** Cookies has been cleared! Reinitialize decorator... */
356       forceCompression = false;
357       cacheControl.init(this);
358     }
359
360
361
362     @Override
363     public boolean isZipped() {
364       return out.isZipped();
365     }
366
367     @Override
368     public long getTimestamp() {
369       return now;
370     }
371
372     @Override
373     public int accepts(HttpServletRequest request) {
374       return status;
375     }
376
377     @Override
378     public int getCacheSeconds(HttpServletRequest request) {
379       return cacheSeconds;
380     }
381
382     @Override
383     public long getLastModified(HttpServletRequest request) {
384       return lastModified;
385     }
386
387     @Override
388     public String getETag(HttpServletRequest request) {
389       return eTag;
390     }
391
392     @Override
393     public boolean isETagWeak() {
394       return weak;
395     }
396
397     @Override
398     public void cacheControl(HttpServletRequest request, Map<String, String> cacheControlMap) {
399       cacheControlMap.putAll(cacheParams);
400     }
401
402     @Override
403     public Map<String,String> getAdditionalHeaders(HttpServletRequest request) {
404       return EMPTY;
405     }
406
407     public void supressGuessing() {
408       guessing = false;
409     }
410
411
412     private void analyzeHeader(String name, String value, boolean overwrite) {
413       if (name == null)
414         return;
415       name = name.trim();
416
417       if (name.equalsIgnoreCase(Headers.HEADER_DATE)) {
418         if (value == null) {
419           if (overwrite) {
420             now = System.currentTimeMillis();
421             cacheSeconds = AcceleratorFilter.this.cacheSeconds;
422           }
423           return;
424         }
425         try {
426           SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
427           now = parser.parse(value).getTime();
428           calculateCacheSeconds();
429         }
430         catch (ParseException e) {
431           log.warn("ignoring date for header \"Date\" in invalid format: {}", value);
432         }
433         return;
434       }
435
436       if (name.equalsIgnoreCase(Headers.HEADER_EXPIRES)) {
437         if (value == null) {
438           if (overwrite) {
439             expires = 0;
440             cacheSeconds = AcceleratorFilter.this.cacheSeconds;
441           }
442           return;
443         }
444         try {
445           SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
446           expires = parser.parse(value).getTime();
447           calculateCacheSeconds();
448         }
449         catch (ParseException e) {
450           log.warn("ignoring date for header \"Expires\" in invalid format: {}", value);
451         }
452         return;
453       }
454
455       if (name.equalsIgnoreCase(Headers.HEADER_LAST_MODIFIED)) {
456         if (value == null) {
457           if (overwrite)
458             lastModified = AcceleratorFilter.this.lastModified;
459           return;
460         }
461         try {
462           SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
463           lastModified = parser.parse(value).getTime();
464         }
465         catch (ParseException e) {
466           log.warn("ignoring date for header \"Last-Modified\" in invalid format: {}", value);
467         }
468         return;
469       }
470
471       if (name.equalsIgnoreCase(Headers.HEADER_ETAG)) {
472         if (value == null) {
473           if (overwrite) {
474             eTag = AcceleratorFilter.this.eTag;
475             weak = AcceleratorFilter.this.weak;
476           }
477           return;
478         }
479         value = value.trim();
480         int start = 0;
481         int end = value.length();
482         if (value.startsWith("W/")) {
483           weak = true;
484           start = 2;
485         }
486         else {
487           weak = false;
488         }
489         if (value.charAt(start) == '"')
490           start++;
491         else
492           log.warn("Quote at the beginning ov ETag is missing: {}", value);
493         if (value.charAt(end -1) == '"')
494           end--;
495         else
496           log.warn("Quote at the end of ETag is missing: {}", value);
497         eTag = value.substring(start, end);
498         String filtered = eTag.replaceAll("[^\\x00-\\x21\\x23-\\x7F]+","");
499         if (filtered.length() < eTag.length()) {
500           log.warn("filtering out illegal characters in ETag: \"{}\" -> \"{}\"", eTag, filtered);
501           eTag = filtered;
502         }
503       }
504
505       if (name.equalsIgnoreCase(Headers.HEADER_CACHE_CONTROL)) {
506         if (overwrite)
507           cacheParams.clear();
508         if (value == null)
509           return;
510         for (String param : value.split(",")) {
511           param = param.trim();
512           int pos = param.indexOf("=");
513           if (pos < 0) {
514             cacheParams.put(param, null);
515           }
516           else {
517             String paramName = param.substring(0, pos).trim();
518             if (paramName.equalsIgnoreCase("max-age")) {
519               try {
520                 cacheSeconds = Integer.parseInt(param.substring(pos + 1));
521                 cacheSecondsSet = true;
522               }
523               catch (NumberFormatException e) {
524                 log.warn("illegal value for Header \"Cache-Control\":", param);
525               }
526             }
527             else {
528               cacheParams.put(paramName, param.substring(pos + 1));
529             }
530           }
531         }
532         return;
533       }
534
535       if (name.equalsIgnoreCase(Headers.HEADER_PRAGMA)) {
536         if (value != null && value.trim().equalsIgnoreCase("no-cache"))
537           cacheSeconds = 0;
538         return;
539       }
540
541       /** Pass header through, if no value from intrest was found */
542       if (overwrite)
543         response.setHeader(name, value);
544       else
545         response.addHeader(name, value);
546     }
547
548     private void calculateCacheSeconds() {
549       if (!cacheSecondsSet && expires >= now) {
550         cacheSeconds = (int)(expires/1000 - now/1000);
551         log.debug("calculating cache-seconds from DATE and EXPIRES: {}", cacheSeconds);
552       }
553     }
554
555     @Override
556     public void addCookie(Cookie cookie) {
557       // TODO: Je nach Vary-Einstellung ETag anpassen?
558       response.addCookie(cookie);
559     }
560
561     @Override
562     public boolean containsHeader(String name) {
563       return response.containsHeader(name);
564     }
565
566     @Override
567     public String encodeURL(String url) {
568       return response.encodeURL(url);
569     }
570
571     @Override
572     public String encodeRedirectURL(String url) {
573       return response.encodeRedirectURL(url);
574     }
575
576     @Override
577     public String encodeUrl(String url) {
578       return response.encodeUrl(url);
579     }
580
581     @Override
582     public String encodeRedirectUrl(String url) {
583       return response.encodeRedirectUrl(url);
584     }
585
586     @Override
587     public void sendError(int sc, String msg) throws IOException {
588       // TODO: Decoration anpassen/anstoßen?!?
589       response.sendError(sc,msg);
590     }
591
592     @Override
593     public void sendError(int sc) throws IOException {
594       // TODO: Decoration anpassen/anstoßen?!?
595       response.sendError(sc);
596     }
597
598     @Override
599     public void sendRedirect(String location) throws IOException {
600       // TODO: Decoration anpassen/anstoßen?!?
601       response.sendRedirect(location);
602     }
603
604     @Override
605     public String getCharacterEncoding() {
606       return response.getCharacterEncoding();
607     }
608
609     @Override
610     public String getContentType() {
611       return response.getContentType();
612     }
613
614     @Override
615     public void setCharacterEncoding(String charset) {
616       // TODO: Je nach Vary-Einstellung ETag anpassen?
617       response.setCharacterEncoding(charset);
618     }
619
620     @Override
621     public void setContentType(String type) {
622       // TODO: Je nach Vary-Einstellung ETag anpassen?
623       response.setContentType(type);
624     }
625
626     @Override
627     public int getBufferSize() {
628       return response.getBufferSize();
629     }
630
631     @Override
632     public boolean isCommitted() {
633       // TODO: Eigene commit-Kontrolle wegen Dekorations-Einstiegspunkt?!?
634       return response.isCommitted();
635     }
636
637     @Override
638     public void setLocale(Locale loc) {
639       // TODO: Je nach Vary-Einstellung ETag anpassen?
640       response.setLocale(loc);
641     }
642
643     @Override
644     public Locale getLocale() {
645       return getLocale();
646     }
647
648
649     class CountingServletOutputStream extends ServletOutputStream {
650
651       private OutputStream out;
652       int left;
653       boolean empty;
654
655
656       CountingServletOutputStream() throws IOException {
657         out = response.getOutputStream();
658         left = buffer;
659         empty = true;
660       }
661
662
663       void setBuffer(int size) throws IllegalStateException {}
664
665       boolean isZipped() {
666         return false;
667       }
668
669       void finish() throws IOException {
670         decorate();
671       }
672
673       void decorate() throws IOException {
674         try {
675           AcceleratorFilter.this.cacheControl.decorate(AccelerationWrapper.this.request, response, AccelerationWrapper.this);
676         }
677         catch (Exception e) {
678           log.error("Error while guessing Cache-Header's", e);
679           response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
680         }
681       }
682
683
684       @Override
685       public void close() throws IOException {
686         decorate();
687         out.close();
688       }
689
690       @Override
691       public void flush() throws IOException {
692         decorate();
693         out.flush();
694       }
695
696       @Override
697       public void write(int i) throws IOException {
698         empty = false;
699         if (left == 0)
700           decorate();
701         left--;
702         out.write(i);
703       }
704     }
705
706
707     final class GZIPServletOutputStream extends CountingServletOutputStream {
708
709       final static int MINIMAL_BUFFER_SIZE = 128;
710
711       private ByteArrayOutputStream buffer;
712       private OutputStream out;
713       private GZIPOutputStream zout;
714       private int bufferSize;
715       private boolean decorated = false;
716
717
718       public GZIPServletOutputStream() throws IOException {
719         setBuffer(AcceleratorFilter.this.buffer);
720       }
721
722
723       @Override
724       void finish() throws IOException {
725         decorate();
726         zout.finish();
727         super.out.write(buffer.toByteArray());
728       }
729
730       @Override
731       boolean isZipped() {
732         if (decorated)
733           return true;
734         return !empty;
735       }
736
737       @Override
738       void setBuffer(int size) throws IllegalStateException {
739         if (!empty)
740           throw new IllegalStateException("attemp to change buffer size after writing data to response!");
741
742         if (size > MINIMAL_BUFFER_SIZE) {
743           left = size;
744           try {
745             bufferSize = size;
746             out = response.getOutputStream();
747             buffer = new ByteArrayOutputStream(size);
748             zout = new GZIPOutputStream(buffer, size);
749             super.out = zout;
750           }
751           catch (IOException e) {
752             throw new IllegalStateException(e);
753           }
754         }
755       }
756
757
758
759       @Override
760       public void close() throws IOException {
761         decorate();
762         zout.close();
763         out.write(buffer.toByteArray());
764         out.close();
765       }
766
767       @Override
768       public void flush() throws IOException {
769         decorate();
770         out.write(buffer.toByteArray());
771         out.flush();
772       }
773
774       @Override
775       public void write(int i) throws IOException {
776         empty = false;
777         if (left == 0) {
778           if (!decorated) {
779             decorate();
780             decorated = true;
781           }
782           out.write(buffer.toByteArray());
783           buffer.reset();
784           left = bufferSize;
785         }
786         left--;
787         zout.write(i);
788       }
789     }
790   }
791 }