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