f17ec8d3477b623b34c1981e993baf248f8b2f67
[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       out.flush();
346     }
347
348     @Override
349     public void addCookie(Cookie cookie) {
350       // TODO: Je nach Vary-Einstellung ETag anpassen?
351       response.addCookie(cookie);
352     }
353
354     @Override
355     public boolean containsHeader(String name) {
356       return response.containsHeader(name);
357     }
358
359     @Override
360     public String encodeURL(String url) {
361       return response.encodeURL(url);
362     }
363
364     @Override
365     public String encodeRedirectURL(String url) {
366       return response.encodeRedirectURL(url);
367     }
368
369     @Override
370     public String encodeUrl(String url) {
371       return response.encodeUrl(url);
372     }
373
374     @Override
375     public String encodeRedirectUrl(String url) {
376       return response.encodeRedirectUrl(url);
377     }
378
379     @Override
380     public void sendError(int sc, String msg) throws IOException {
381       response.sendError(sc,msg);
382     }
383
384     @Override
385     public void sendError(int sc) throws IOException {
386       response.sendError(sc);
387     }
388
389     @Override
390     public void sendRedirect(String location) throws IOException {
391       response.sendRedirect(location);
392     }
393
394     @Override
395     public String getCharacterEncoding() {
396       return response.getCharacterEncoding();
397     }
398
399     @Override
400     public String getContentType() {
401       return response.getContentType();
402     }
403
404     @Override
405     public void setCharacterEncoding(String charset) {
406       // TODO: Je nach Vary-Einstellung ETag anpassen?
407       response.setCharacterEncoding(charset);
408     }
409
410     @Override
411     public void setContentType(String type) {
412       // TODO: Je nach Vary-Einstellung ETag anpassen?
413       response.setContentType(type);
414     }
415
416     @Override
417     public boolean isCommitted() {
418       return committed;
419     }
420
421     @Override
422     public void setLocale(Locale loc) {
423       // TODO: Je nach Vary-Einstellung ETag anpassen?
424       response.setLocale(loc);
425     }
426
427     @Override
428     public Locale getLocale() {
429       return getLocale();
430     }
431
432
433
434     @Override
435     public boolean isZipped() {
436       return zipped;
437     }
438
439     @Override
440     public long getTimestamp() {
441       return now;
442     }
443
444     @Override
445     public int accepts(HttpServletRequest request) {
446       return status;
447     }
448
449     @Override
450     public int getCacheSeconds(HttpServletRequest request) {
451       return cacheSeconds;
452     }
453
454     @Override
455     public long getLastModified(HttpServletRequest request) {
456       return lastModified;
457     }
458
459     @Override
460     public String getETag(HttpServletRequest request) {
461       return eTag;
462     }
463
464     @Override
465     public boolean isETagWeak() {
466       return weak;
467     }
468
469     @Override
470     public void cacheControl(HttpServletRequest request, Map<String, String> cacheControlMap) {
471       cacheControlMap.putAll(cacheParams);
472     }
473
474     @Override
475     public Map<String,String> getAdditionalHeaders(HttpServletRequest request) {
476       return EMPTY;
477     }
478
479     public void supressGuessing() {
480       guessing = false;
481     }
482
483
484     private void analyzeHeader(String name, String value, boolean overwrite) {
485       if (name == null)
486         return;
487       name = name.trim();
488
489       if (name.equalsIgnoreCase(Headers.HEADER_DATE)) {
490         if (value == null) {
491           if (overwrite) {
492             now = System.currentTimeMillis();
493             cacheSeconds = AcceleratorFilter.this.cacheSeconds;
494           }
495           return;
496         }
497         try {
498           SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
499           now = parser.parse(value).getTime();
500           calculateCacheSeconds();
501         }
502         catch (ParseException e) {
503           log.warn("ignoring date for header \"Date\" in invalid format: {}", value);
504         }
505         return;
506       }
507
508       if (name.equalsIgnoreCase(Headers.HEADER_EXPIRES)) {
509         if (value == null) {
510           if (overwrite) {
511             expires = 0;
512             cacheSeconds = AcceleratorFilter.this.cacheSeconds;
513           }
514           return;
515         }
516         try {
517           SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
518           expires = parser.parse(value).getTime();
519           calculateCacheSeconds();
520         }
521         catch (ParseException e) {
522           log.warn("ignoring date for header \"Expires\" in invalid format: {}", value);
523         }
524         return;
525       }
526
527       if (name.equalsIgnoreCase(Headers.HEADER_LAST_MODIFIED)) {
528         if (value == null) {
529           if (overwrite)
530             lastModified = AcceleratorFilter.this.lastModified;
531           return;
532         }
533         try {
534           SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
535           lastModified = parser.parse(value).getTime();
536         }
537         catch (ParseException e) {
538           log.warn("ignoring date for header \"Last-Modified\" in invalid format: {}", value);
539         }
540         return;
541       }
542
543       if (name.equalsIgnoreCase(Headers.HEADER_ETAG)) {
544         if (value == null) {
545           if (overwrite) {
546             eTag = AcceleratorFilter.this.eTag;
547             weak = AcceleratorFilter.this.weak;
548           }
549           return;
550         }
551         value = value.trim();
552         int start = 0;
553         int end = value.length();
554         if (value.startsWith("W/")) {
555           weak = true;
556           start = 2;
557         }
558         else {
559           weak = false;
560         }
561         if (value.charAt(start) == '"')
562           start++;
563         else
564           log.warn("Quote at the beginning ov ETag is missing: {}", value);
565         if (value.charAt(end -1) == '"')
566           end--;
567         else
568           log.warn("Quote at the end of ETag is missing: {}", value);
569         eTag = value.substring(start, end);
570         String filtered = eTag.replaceAll("[^\\x00-\\x21\\x23-\\x7F]+","");
571         if (filtered.length() < eTag.length()) {
572           log.warn("filtering out illegal characters in ETag: \"{}\" -> \"{}\"", eTag, filtered);
573           eTag = filtered;
574         }
575       }
576
577       if (name.equalsIgnoreCase(Headers.HEADER_CACHE_CONTROL)) {
578         if (overwrite)
579           cacheParams.clear();
580         if (value == null)
581           return;
582         for (String param : value.split(",")) {
583           param = param.trim();
584           int pos = param.indexOf("=");
585           if (pos < 0) {
586             cacheParams.put(param, null);
587           }
588           else {
589             String paramName = param.substring(0, pos).trim();
590             if (paramName.equalsIgnoreCase("max-age")) {
591               try {
592                 cacheSeconds = Integer.parseInt(param.substring(pos + 1));
593                 cacheSecondsSet = true;
594               }
595               catch (NumberFormatException e) {
596                 log.warn("illegal value for Header \"Cache-Control\":", param);
597               }
598             }
599             else {
600               cacheParams.put(paramName, param.substring(pos + 1));
601             }
602           }
603         }
604         return;
605       }
606
607       if (name.equalsIgnoreCase(Headers.HEADER_PRAGMA)) {
608         if (value != null && value.trim().equalsIgnoreCase("no-cache"))
609           cacheSeconds = 0;
610         return;
611       }
612
613       /** Pass header through, if no value from intrest was found */
614       if (overwrite)
615         response.setHeader(name, value);
616       else
617         response.addHeader(name, value);
618     }
619
620     private void calculateCacheSeconds() {
621       if (!cacheSecondsSet && expires >= now) {
622         cacheSeconds = (int)(expires/1000 - now/1000);
623         log.debug("calculating cache-seconds from DATE and EXPIRES: {}", cacheSeconds);
624       }
625     }
626
627
628     private class AcceleratorServletOutputStream extends ServletOutputStream  {
629
630       private final ServletOutputStream sos;
631
632
633       private AcceleratorServletOutputStream() throws IOException {
634         bufferSize = defaultBufferSize;
635         buffer = new byte[bufferSize];
636         sos = AccelerationWrapper.this.response.getOutputStream();
637       }
638
639
640       private OutputStream out() throws IOException {
641         if (os == null)
642           os = zipped ? new GZIPOutputStream(sos) : sos;
643         return os;
644       }
645
646       @Override
647       public void write(int i) throws IOException {
648         if (pos == bufferSize) {
649           out().write(buffer);
650           committed = true;
651           /** Dekoration nur beim ersten Schreib-Schub anstoßen */
652           if (pos == size) {
653             if (!cacheControl.decorate(request, response)) {
654               zipped = false;
655               os = null;
656               pos = 0;
657               throw new NotModifiedException();
658             }
659           }
660           pos = 0;
661         }
662         buffer[pos++] = (byte) i;
663         size++;
664       }
665
666       @Override
667       public void flush() throws IOException {
668         if (pos == 0)
669           return;
670
671         committed = true;
672         /** Dekoration nur beim ersten Schreib-Schub anstoßen */
673         if (pos == size) {
674           if (!cacheControl.decorate(request, response)) {
675             zipped = false;
676             os = null;
677             pos = 0;
678             throw new NotModifiedException();
679           }
680         }
681         out().write(buffer, 0, pos);
682         out().flush();
683         pos = 0;
684       }
685
686       @Override
687       public void close() throws IOException {
688         if (size == 0) {
689           committed = true;
690           zipped = false;
691           if (!cacheControl.decorate(request, response))
692             throw new NotModifiedException();
693           sos.close();
694         }
695         else {
696           flush();
697           out().close();
698         }
699       }
700     }
701   }
702 }
703
704 class NotModifiedException extends IOException {}