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