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