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