Unterprojekt Utils von Fotos in eigenständiges Projekt Juplo verwandelt
[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         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       }
118
119       if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) {
120         log.debug("{}: ETag {} not changed -> 304 ", request.getRequestURI(), ifNoneMatch);
121         response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
122         return false;
123       }
124
125       /** HTTP/1.1-Caching-Header richtig setzen!! */
126       response.setDateHeader(HEADER_LAST_MODIFIED, lastModified);
127
128       /** Cache-Control für HTTP/1.1-Clients generieren */
129       Map<String, String> cacheControl = new HashMap<String, String>(cacheable.getCacheControl(request));
130
131       /**
132        * Wenn eins JSESSIONID in der URL enthalten ist, darf die Anfrage nur vom
133        * Browser gecached werden!
134        */
135       if (request.isRequestedSessionIdFromURL()) {
136         cacheSeconds = 0;
137         cacheControl.put("private", null);
138       }
139
140       if (cacheControl.containsKey("private")) {
141         /**
142          * HTTP/1.0 Caches davon abhalten, die Ressource zu cachen (vgl.: RFC
143          * 2616, {@plainlink
144          * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
145          * Abschnitt 14.9.3} und {@plainlink
146          * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.32
147          * Abschnitt 14.32})
148          */
149         response.setDateHeader(HEADER_EXPIRES, 0l);
150         response.addHeader(HEADER_PRAGMA, "no-cache");
151       }
152       else {
153         /**
154          * Hier muss nicht geprüft werden, ob cacheSeconds > 0 gilt, da in diesem
155          * Fall oben bereits No-Cache-Header generiert und <code>false</code>
156          * zurückgeliefert werden!
157          *
158          * Den Wert als <code>max-age</code> zu den Schlüssel-Wert-Paaren für den
159          * <code>Cache-Control</code>-Header hinzufügen und einen entsprechenden
160          * <code>Expires</code>-Header für HTTP/1.0-Clients setzen.
161          */
162         cacheControl.put("max-age", Integer.toString(cacheSeconds));
163         response.setDateHeader(HEADER_EXPIRES, (now + cacheSeconds * 1000));
164       }
165
166       StringBuilder builder = new StringBuilder();
167       for (Entry<String, String> entry : cacheControl.entrySet()) {
168         builder.setLength(0);
169         builder.append(entry.getKey());
170         if (entry.getValue() != null) {
171           builder.append('=');
172           builder.append(entry.getValue());
173         }
174         response.addHeader(HEADER_CACHE_CONTROL, builder.toString());
175       }
176
177       return true;
178     }
179     catch (ClassCastException e) {
180       return true;
181     }
182   }
183
184   @Override
185   public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {}
186
187   @Override
188   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {}
189 }