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