1 package de.halbekunst.juplo.cachecontrol;
3 import java.io.IOException;
4 import java.io.PrintWriter;
5 import java.text.ParseException;
6 import java.text.SimpleDateFormat;
7 import java.util.Collections;
8 import java.util.Enumeration;
9 import java.util.HashMap;
10 import java.util.Locale;
12 import java.util.zip.GZIPOutputStream;
13 import javax.servlet.Filter;
14 import javax.servlet.FilterChain;
15 import javax.servlet.FilterConfig;
16 import javax.servlet.ServletException;
17 import javax.servlet.ServletOutputStream;
18 import javax.servlet.ServletRequest;
19 import javax.servlet.ServletResponse;
20 import javax.servlet.http.HttpServletRequest;
21 import javax.servlet.http.HttpServletResponse;
22 import javax.servlet.http.HttpServletResponseWrapper;
23 import org.slf4j.Logger;
24 import org.slf4j.LoggerFactory;
25 import org.springframework.beans.factory.annotation.Autowire;
26 import org.springframework.beans.factory.annotation.Autowired;
27 import org.springframework.beans.factory.annotation.Configurable;
35 @Configurable(autowire=Autowire.BY_NAME)
36 public class AcceleratorFilter implements Filter {
37 private final static Logger log = LoggerFactory.getLogger(AcceleratorFilter.class);
39 private final static Map<String,String> EMPTY = Collections.unmodifiableMap(new HashMap<String,String>());
41 public final static String REQUEST_URI_ATTRIBUTE = "javax.servlet.include.request_uri";
42 public final static String RESPONSE_WRAPPER = AcceleratorFilter.class.getName() + ".RESPONSE_WRAPPER";
45 @Autowired CacheControl cacheControl;
46 @Autowired(required=true) Integer buffer;
47 @Autowired(required=true) String eTag;
48 @Autowired(required=true) Boolean weak;
49 @Autowired(required=true) Long lastModified;
50 @Autowired(required=true) Integer cacheSeconds;
54 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
55 if (!(request instanceof HttpServletRequest)) {
56 log.error("AcceleratorFilter can only handle HTTP-requests");
57 chain.doFilter(request, response);
61 /** Prüfen, ob es sich um eine Anfrage für einen JSP-Include handelt */
62 if (request.getAttribute(REQUEST_URI_ATTRIBUTE) != null) {
63 log.debug("Includes cannot be accelerated");
64 chain.doFilter(request, response);
68 HttpServletRequest httpRequest = (HttpServletRequest)request;
69 HttpServletResponse httpResponse = (HttpServletResponse)response;
70 AccelerationWrapper wrapper = new AccelerationWrapper(httpRequest, httpResponse);
71 httpRequest.setAttribute(RESPONSE_WRAPPER, wrapper);
72 cacheControl.init(wrapper);
73 chain.doFilter(request, wrapper);
74 /** Dekoration auslösen, falls sie bisher nicht ausgelöst wurde... */
75 cacheControl.decorate(httpRequest, httpResponse, wrapper);
79 public void init(FilterConfig filterConfig) throws ServletException {
83 public void destroy() {
87 class AccelerationWrapper extends HttpServletResponseWrapper implements CacheMethodHandle {
89 private final HttpServletRequest request;
90 private final HttpServletResponse response;
92 boolean zipped; // CacheControll greift direkt auf dieses Flag zu!
94 private CountingServletOutputStream out;
95 private ServletOutputStream stream;
96 private PrintWriter writer;
98 private boolean guessing = true;
102 private int cacheSeconds;
103 private boolean cacheSecondsSet = false;
104 private long lastModified, expires = 0l;
106 private boolean weak;
107 private Map<String,String> cacheParams;
110 AccelerationWrapper(HttpServletRequest request, HttpServletResponse response) throws IOException {
113 this.request = request;
114 this.response = response;
116 now = System.currentTimeMillis();
117 status = HttpServletResponse.SC_OK;
118 cacheSeconds = AcceleratorFilter.this.cacheSeconds;
119 lastModified = AcceleratorFilter.this.lastModified;
120 eTag = AcceleratorFilter.this.eTag;
121 weak = AcceleratorFilter.this.weak;
122 cacheParams = new HashMap<String,String>();
125 Enumeration values = request.getHeaders(Headers.HEADER_ACCEPT_ENCODING);
126 while (values.hasMoreElements()) {
127 String value = (String) values.nextElement();
128 if (value.indexOf("gzip") != -1) {
134 out = new GZIPServletOutputStream();
136 out = new WrappedServletOutputStream();
141 public void setStatus(int sc) {
142 response.setStatus(sc);
145 cacheControl.decorate(request, response, this);
147 catch (Exception e) {
148 log.error("Error while decorating response", e);
153 public void setStatus(int sc, String sm) {
154 response.setStatus(sc,sm);
157 cacheControl.decorate(request, response, this);
159 catch (Exception e) {
160 log.error("Error while decorating response", e);
165 public void addDateHeader(String name, long value) {
168 super.addDateHeader(name, value);
172 if (Headers.HEADER_DATE.equalsIgnoreCase(name)) {
174 calculateCacheSeconds();
178 if (Headers.HEADER_EXPIRES.equalsIgnoreCase(name)) {
180 calculateCacheSeconds();
184 if (Headers.HEADER_LAST_MODIFIED.equalsIgnoreCase(name)) {
185 lastModified = value;
189 /** Unknown header: pass throug! */
190 super.addDateHeader(name, value);
194 public void addHeader(String name, String value) {
197 super.addHeader(name, value);
203 analyzeHeader(name, value, false);
207 public void addIntHeader(String name, int value) {
210 super.addIntHeader(name, value);
214 analyzeHeader(name, Integer.toString(value), false);
218 public void setDateHeader(String name, long value) {
221 super.setDateHeader(name, value);
225 if (Headers.HEADER_DATE.equalsIgnoreCase(name)) {
227 calculateCacheSeconds();
231 if (Headers.HEADER_EXPIRES.equalsIgnoreCase(name)) {
233 calculateCacheSeconds();
237 if (Headers.HEADER_LAST_MODIFIED.equalsIgnoreCase(name)) {
238 lastModified = value;
242 /** Unknown header: pass throug! */
243 super.setDateHeader(name, value);
247 public void setHeader(String name, String value) {
250 super.setHeader(name, value);
254 analyzeHeader(name, value, true);
258 public void setIntHeader(String name, int value) {
261 super.setIntHeader(name, value);
265 analyzeHeader(name, Integer.toString(value), true);
269 public ServletOutputStream getOutputStream() throws IOException {
272 throw new IllegalStateException("ServletOutputStream and PrintWriter cannot be requested both!");
274 if (stream == null) {
282 public PrintWriter getWriter() throws IOException {
285 throw new IllegalStateException("ServletOutputStream and PrintWriter cannot be requested both!");
287 if (writer == null) {
288 writer = new PrintWriter(out);
295 public void setContentLength(int len) {
297 log.info("Supressing explicit content-length {} for request {}, because content will be zipped!", len, request.getRequestURI());
299 response.setContentLength(len);
303 public void setBufferSize(int size) {
306 response.setBufferSize(size);
310 public void flushBuffer() throws IOException {
312 cacheControl.decorate(request, response, this);
313 response.flushBuffer();
317 public void resetBuffer() {
319 response.resetBuffer();
325 public void reset() {
330 out = new GZIPServletOutputStream();
332 out = new WrappedServletOutputStream();
334 catch (IOException e) {
335 throw new IllegalStateException(e);
340 /** Cookies has been cleared! Reinitialize decorator... */
341 cacheControl.init(this);
347 public boolean isZipped() {
348 return out.isZipped();
352 public long getTimestamp() {
357 public int accepts(HttpServletRequest request) {
362 public int getCacheSeconds(HttpServletRequest request) {
367 public long getLastModified(HttpServletRequest request) {
372 public String getETag(HttpServletRequest request) {
377 public boolean isETagWeak() {
382 public void cacheControl(HttpServletRequest request, Map<String, String> cacheControlMap) {
383 cacheControlMap.putAll(cacheParams);
387 public Map<String,String> getAdditionalHeaders(HttpServletRequest request) {
391 public void supressGuessing() {
396 private void analyzeHeader(String name, String value, boolean overwrite) {
401 if (name.equalsIgnoreCase(Headers.HEADER_DATE)) {
404 now = System.currentTimeMillis();
405 cacheSeconds = AcceleratorFilter.this.cacheSeconds;
410 SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
411 now = parser.parse(value).getTime();
412 calculateCacheSeconds();
414 catch (ParseException e) {
415 log.warn("ignoring date for header \"Date\" in invalid format: {}", value);
420 if (name.equalsIgnoreCase(Headers.HEADER_EXPIRES)) {
424 cacheSeconds = AcceleratorFilter.this.cacheSeconds;
429 SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
430 expires = parser.parse(value).getTime();
431 calculateCacheSeconds();
433 catch (ParseException e) {
434 log.warn("ignoring date for header \"Expires\" in invalid format: {}", value);
439 if (name.equalsIgnoreCase(Headers.HEADER_LAST_MODIFIED)) {
442 lastModified = AcceleratorFilter.this.lastModified;
446 SimpleDateFormat parser = new SimpleDateFormat(Headers.RFC_1123_DATE_FORMAT, Locale.US);
447 lastModified = parser.parse(value).getTime();
449 catch (ParseException e) {
450 log.warn("ignoring date for header \"Last-Modified\" in invalid format: {}", value);
455 if (name.equalsIgnoreCase(Headers.HEADER_ETAG)) {
458 eTag = AcceleratorFilter.this.eTag;
459 weak = AcceleratorFilter.this.weak;
463 value = value.trim();
465 int end = value.length();
466 if (value.startsWith("W/")) {
473 if (value.charAt(start) == '"')
476 log.warn("Quote at the beginning ov ETag is missing: {}", value);
477 if (value.charAt(end -1) == '"')
480 log.warn("Quote at the end of ETag is missing: {}", value);
481 eTag = value.substring(start, end);
482 String filtered = eTag.replaceAll("[^\\x00-\\x21\\x23-\\x7F]+","");
483 if (filtered.length() < eTag.length()) {
484 log.warn("filtering out illegal characters in ETag: \"{}\" -> \"{}\"", eTag, filtered);
489 if (name.equalsIgnoreCase(Headers.HEADER_CACHE_CONTROL)) {
494 for (String param : value.split(",")) {
495 param = param.trim();
496 int pos = param.indexOf("=");
498 cacheParams.put(param, null);
501 String paramName = param.substring(0, pos).trim();
502 if (paramName.equalsIgnoreCase("max-age")) {
504 cacheSeconds = Integer.parseInt(param.substring(pos + 1));
505 cacheSecondsSet = true;
507 catch (NumberFormatException e) {
508 log.warn("illegal value for Header \"Cache-Control\":", param);
512 cacheParams.put(paramName, param.substring(pos + 1));
519 if (name.equalsIgnoreCase(Headers.HEADER_PRAGMA)) {
520 if (value != null && value.trim().equalsIgnoreCase("no-cache"))
525 /** Pass header through, if no value from intrest was found */
527 super.setHeader(name, value);
529 super.addHeader(name, value);
532 private void calculateCacheSeconds() {
533 if (!cacheSecondsSet && expires >= now) {
534 cacheSeconds = (int)(expires/1000 - now/1000);
535 log.debug("calculating cache-seconds from DATE and EXPIRES: {}", cacheSeconds);
540 abstract class CountingServletOutputStream extends ServletOutputStream {
542 abstract void setBuffer(int size) throws IllegalStateException;
543 abstract boolean isZipped();
547 final class GZIPServletOutputStream extends CountingServletOutputStream {
549 final static int MINMAL_BUFFER_SIZE = 128;
552 private final GZIPOutputStream out;
553 private int buffer, left;
554 private boolean empty;
557 public GZIPServletOutputStream() throws IOException {
558 this.out = new GZIPOutputStream(response.getOutputStream());
560 setBuffer(AcceleratorFilter.this.buffer);
571 void setBuffer(int size) {
573 throw new IllegalStateException("attemp to change buffer size after writing data to response!");
575 if (size > MINMAL_BUFFER_SIZE) {
583 public void close() throws IOException {
585 AcceleratorFilter.this.cacheControl.decorate(AccelerationWrapper.this.request, response, AccelerationWrapper.this);
587 catch (Exception e) {
588 log.error("Error while guessing Cache-Header's", e);
589 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
596 public void flush() throws IOException {
598 AcceleratorFilter.this.cacheControl.decorate(AccelerationWrapper.this.request, response, AccelerationWrapper.this);
600 catch (Exception e) {
601 log.error("Error while guessing Cache-Header's", e);
602 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
609 public void write(int i) throws IOException {
612 AcceleratorFilter.this.cacheControl.decorate(AccelerationWrapper.this.request, response, AccelerationWrapper.this);
614 catch (Exception e) {
615 log.error("Error while guessing Cache-Header's", e);
616 response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
626 final class WrappedServletOutputStream extends CountingServletOutputStream {
628 private final ServletOutputStream out;
629 private boolean empty;
632 public WrappedServletOutputStream() throws IOException {
633 this.out = response.getOutputStream();
644 void setBuffer(int size) {
646 throw new IllegalStateException("attemp to change buffer size after writing data to response!");
651 public void close() throws IOException {
656 public void flush() throws IOException {
661 public void write(int i) throws IOException {