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