bfdf97ab1003a3eaacf6dd58a458e2f79a05692e
[percentcodec] / cachecontrol / src / main / java / de / halbekunst / juplo / cachecontrol / CacheControlInterceptor.java
1 package de.halbekunst.juplo.cachecontrol;
2
3 import java.util.Date;
4 import java.util.HashMap;
5 import java.util.Map;
6 import java.util.Map.Entry;
7 import javax.servlet.http.HttpServletRequest;
8 import javax.servlet.http.HttpServletResponse;
9 import org.slf4j.Logger;
10 import org.slf4j.LoggerFactory;
11 import org.springframework.web.servlet.HandlerInterceptor;
12 import org.springframework.web.servlet.ModelAndView;
13
14 /**
15  *
16  * @author kai
17  */
18 public class CacheControlInterceptor implements HandlerInterceptor {
19   private final static Logger log = LoggerFactory.getLogger(CacheControlInterceptor.class);
20
21   public static final String HEADER_DATE = "Date";
22   public static final String HEADER_CACHE_CONTROL = "Cache-Control";
23   public static final String HEADER_LAST_MODIFIED = "Last-Modified";
24   public static final String HEADER_ETAG = "ETag";
25   public static final String HEADER_EXPIRES = "Expires";
26   public static final String HEADER_PRAGMA = "Pragma";
27   public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
28   public static final String HEADER_IF_NONE_MATCH = "If-None-Match";
29
30
31   @Override
32   public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
33     try {
34       Cacheable cacheable = (Cacheable)handler;
35
36       long now = System.currentTimeMillis();
37
38       /**
39        * Alle Antworten (insbesondere auch 304) sollen nach dem {@plainlink
40        * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18 RFC 2616,
41        * Abschnitt 14.18} einen Date-Header enthalten
42        */
43       response.setDateHeader(HEADER_DATE, now);
44
45       /** Prüfen, ob der Handler willig ist, den Request zu verarbeiten */
46       if (!cacheable.accepts(request)) {
47         response.sendError(HttpServletResponse.SC_NOT_FOUND);
48         return false;
49       }
50
51       /** Nichts weiter unternehmen, wenn der Handler dies nicht will */
52       if (!cacheable.isGenerateCacheHeaders(request))
53         return true;
54
55       int cacheSeconds = cacheable.getCacheSeconds(request);
56       if (cacheSeconds < 0) {
57         log.debug("{}: caching disabled!", request.getRequestURI());
58         response.setDateHeader(HEADER_DATE, now);
59         response.setDateHeader(HEADER_EXPIRES, 0);
60         response.addHeader(HEADER_PRAGMA, "no-cache");
61         response.addHeader(HEADER_CACHE_CONTROL, "private");
62         response.addHeader(HEADER_CACHE_CONTROL, "no-cache");
63         response.addHeader(HEADER_CACHE_CONTROL, "no-store");
64         response.addHeader(HEADER_CACHE_CONTROL, "max-age=0");
65         response.addHeader(HEADER_CACHE_CONTROL, "s-max-age=0");
66         return true;
67       }
68
69       long ifModifiedSince = -1;
70       try {
71         ifModifiedSince = request.getDateHeader(HEADER_IF_MODIFIED_SINCE);
72       }
73       catch (Exception e) {
74         log.error("Exception while fetching If-Modified-Since: {}", e);
75       }
76
77       long lastModified = cacheable.getLastModified(request);
78
79       /**
80        * Sicherstellen, dass der Wert keine Millisekunden enthält, da die
81        * Zeitangabe aus dem Modified-Since-Header keine Millisekunden enthalten
82        * kann und der Test unten dann stets fehlschlagen würde!
83        */
84       lastModified = lastModified - (lastModified % 1000);
85
86       String ifNoneMatch = request.getHeader(HEADER_IF_NONE_MATCH);
87       String eTag = cacheable.getETag(request);
88
89       /**
90        * 304-Antworten sollen nach dem {@plainlink
91        * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 RFC
92        * 2616, Abschnitt 10.3.5} einen ETag-Header enthalten, wenn auch die
93        * 200-Antwort einen enthalten hätte.
94        */
95       if (eTag != null) {
96         response.setHeader(HEADER_ETAG, eTag);
97       }
98
99
100       if (ifModifiedSince >= lastModified && lastModified > 0) {
101         /**
102          * request.getDateHeader liefert die Zeit als long, oder -1, wenn der
103          * Header nicht existiert. D.h., wenn "If-Modified-Since" nicht gesetzt
104          * ist, wird die komplette Seite ausgeliefert.
105          * Der zusätzliche Test, ob lastModified größer 0 ist, ist nötig, um
106          * Fehler auszuschließen, wenn die Implementierung von Cachable
107          * negative Werte für Last-Modified zurückliefert.
108          */
109         if (log.isDebugEnabled())
110           log.debug("Not modified since {}: {}", new Date(ifModifiedSince), request.getRequestURI());
111
112         if (ifNoneMatch == null) {
113           /** Neue Anfrage oder HTTP/1.0 Client! */
114           log.debug("ETag nicht gesetzt: 304 {}", request.getRequestURI());
115           response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
116           return false;
117         }
118       }
119
120       if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) {
121         log.debug("{}: ETag {} not changed -> 304 ", request.getRequestURI(), ifNoneMatch);
122         response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
123         return false;
124       }
125
126
127       log.debug("{}: first up!", request.getRequestURI());
128
129       /** HTTP/1.1-Caching-Header richtig setzen!! */
130       response.setDateHeader(HEADER_LAST_MODIFIED, lastModified);
131
132       /** Cache-Control für HTTP/1.1-Clients generieren */
133       Map<String, String> cacheControl = new HashMap<String, String>(cacheable.getCacheControl(request));
134
135       /**
136        * Wenn eins JSESSIONID in der URL enthalten ist, darf die Anfrage nur vom
137        * Browser gecached werden!
138        */
139       if (request.isRequestedSessionIdFromURL()) {
140         cacheSeconds = 0;
141         cacheControl.put("private", null);
142       }
143
144       if (cacheControl.containsKey("private")) {
145         /**
146          * HTTP/1.0 Caches davon abhalten, die Ressource zu cachen (vgl.: RFC
147          * 2616, {@plainlink
148          * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
149          * Abschnitt 14.9.3} und {@plainlink
150          * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.32
151          * Abschnitt 14.32})
152          */
153         response.setDateHeader(HEADER_EXPIRES, 0l);
154         response.addHeader(HEADER_PRAGMA, "no-cache");
155       }
156       else {
157         /**
158          * Hier muss nicht geprüft werden, ob cacheSeconds > 0 gilt, da in diesem
159          * Fall oben bereits No-Cache-Header generiert und <code>false</code>
160          * zurückgeliefert werden!
161          *
162          * Den Wert als <code>max-age</code> zu den Schlüssel-Wert-Paaren für den
163          * <code>Cache-Control</code>-Header hinzufügen und einen entsprechenden
164          * <code>Expires</code>-Header für HTTP/1.0-Clients setzen.
165          */
166         cacheControl.put("max-age", Integer.toString(cacheSeconds));
167         response.setDateHeader(HEADER_EXPIRES, (now + cacheSeconds * 1000));
168       }
169
170       StringBuilder builder = new StringBuilder();
171       for (Entry<String, String> entry : cacheControl.entrySet()) {
172         builder.setLength(0);
173         builder.append(entry.getKey());
174         if (entry.getValue() != null) {
175           builder.append('=');
176           builder.append(entry.getValue());
177         }
178         response.addHeader(HEADER_CACHE_CONTROL, builder.toString());
179       }
180
181       return true;
182     }
183     catch (ClassCastException e) {
184       return true;
185     }
186   }
187
188   @Override
189   public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}
190
191   @Override
192   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
193 }