Fehler bei ungültigem If-Modified-Since in CacheControlInterceptor behoben
[percentcodec] / utils / cachecontrol / src / main / java / de / halbekunst / utils / cachecontrol / CacheControlInterceptor.java
1 package de.halbekunst.utils.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         response.setDateHeader(HEADER_DATE, now);
58         response.setDateHeader(HEADER_EXPIRES, 0);
59         response.addHeader(HEADER_PRAGMA, "no-cache");
60         response.addHeader(HEADER_CACHE_CONTROL, "private");
61         response.addHeader(HEADER_CACHE_CONTROL, "no-cache");
62         response.addHeader(HEADER_CACHE_CONTROL, "no-store");
63         response.addHeader(HEADER_CACHE_CONTROL, "max-age=0");
64         response.addHeader(HEADER_CACHE_CONTROL, "s-max-age=0");
65         return true;
66       }
67
68       long ifModifiedSince = -1;
69       try {
70         ifModifiedSince = request.getDateHeader(HEADER_IF_MODIFIED_SINCE);
71       }
72       catch (Exception e) {
73         log.error("Exception while fetching If-Modified-Since: {}", e);
74       }
75
76       long lastModified = cacheable.getLastModified(request);
77
78       /**
79        * Sicherstellen, dass der Wert keine Millisekunden enthält, da die
80        * Zeitangabe aus dem Modified-Since-Header keine Millisekunden enthalten
81        * kann und der Test unten dann stets fehlschlagen würde!
82        */
83       lastModified = lastModified - (lastModified % 1000);
84
85       String ifNoneMatch = request.getHeader(HEADER_IF_NONE_MATCH);
86       String eTag = cacheable.getETag(request);
87
88       /**
89        * 304-Antworten sollen nach dem {@plainlink
90        * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 RFC
91        * 2616, Abschnitt 10.3.5} einen ETag-Header enthalten, wenn auch die
92        * 200-Antwort einen enthalten hätte.
93        */
94       if (eTag != null) {
95         response.setHeader(HEADER_ETAG, eTag);
96       }
97
98
99       if (ifModifiedSince >= lastModified && lastModified > 0) {
100         /**
101          * request.getDateHeader liefert die Zeit als long, oder -1, wenn der
102          * Header nicht existiert. D.h., wenn "If-Modified-Since" nicht gesetzt
103          * ist, wird die komplette Seite ausgeliefert.
104          * Der zusätzliche Test, ob lastModified größer 0 ist, ist nötig, um
105          * Fehler auszuschließen, wenn die Implementierung von Cachable
106          * negative Werte für Last-Modified zurückliefert.
107          */
108         if (log.isDebugEnabled())
109           log.debug("Not modified since {}: {}", new Date(ifModifiedSince), request.getRequestURI());
110
111         if (ifNoneMatch == null) {
112           /** Neue Anfrage oder HTTP/1.0 Client! */
113           log.debug("ETag nicht gesetzt: 304 {}", request.getRequestURI());
114           response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
115           return false;
116         }
117         else {
118           if (ifNoneMatch.equals(eTag)) {
119             log.debug("ETag {} not changed: 304 {}", ifNoneMatch, request.getRequestURI());
120             response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
121             return false;
122           }
123         }
124       }
125
126       /** HTTP/1.1-Caching-Header richtig setzen!! */
127       response.setDateHeader(HEADER_LAST_MODIFIED, lastModified);
128
129       /** Cache-Control für HTTP/1.1-Clients generieren */
130       Map<String, String> cacheControl = new HashMap<String, String>(cacheable.getCacheControl(request));
131
132       /**
133        * Wenn eins JSESSIONID in der URL enthalten ist, darf die Anfrage nur vom
134        * Browser gecached werden!
135        */
136       if (request.isRequestedSessionIdFromURL()) {
137         cacheSeconds = 0;
138         cacheControl.put("private", null);
139       }
140
141       if (cacheControl.containsKey("private")) {
142         /**
143          * HTTP/1.0 Caches davon abhalten, die Ressource zu cachen (vgl.: RFC
144          * 2616, {@plainlink
145          * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
146          * Abschnitt 14.9.3} und {@plainlink
147          * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.32
148          * Abschnitt 14.32})
149          */
150         response.setDateHeader(HEADER_EXPIRES, 0l);
151         response.addHeader(HEADER_PRAGMA, "no-cache");
152       }
153       else {
154         /**
155          * Hier muss nicht geprüft werden, ob cacheSeconds > 0 gilt, da in diesem
156          * Fall oben bereits No-Cache-Header generiert und <code>false</code>
157          * zurückgeliefert werden!
158          *
159          * Den Wert als <code>max-age</code> zu den Schlüssel-Wert-Paaren für den
160          * <code>Cache-Control</code>-Header hinzufügen und einen entsprechenden
161          * <code>Expires</code>-Header für HTTP/1.0-Clients setzen.
162          */
163         cacheControl.put("max-age", Integer.toString(cacheSeconds));
164         response.setDateHeader(HEADER_EXPIRES, (now + cacheSeconds * 1000));
165       }
166
167       StringBuilder builder = new StringBuilder();
168       for (Entry<String, String> entry : cacheControl.entrySet()) {
169         builder.setLength(0);
170         builder.append(entry.getKey());
171         if (entry.getValue() != null) {
172           builder.append('=');
173           builder.append(entry.getValue());
174         }
175         response.addHeader(HEADER_CACHE_CONTROL, builder.toString());
176       }
177
178       return true;
179     }
180     catch (ClassCastException e) {
181       return true;
182     }
183   }
184
185   @Override
186   public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}
187
188   @Override
189   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
190 }