CacheControl so umgebaut, dass es sich über Annotationen einbinden lässt
[percentcodec] / cachecontrol / src / main / java / de / halbekunst / juplo / cachecontrol / CacheControl.java
1 package de.halbekunst.juplo.cachecontrol;
2
3 import de.halbekunst.juplo.cachecontrol.annotations.CacheSeconds;
4 import de.halbekunst.juplo.cachecontrol.annotations.Accepts;
5 import de.halbekunst.juplo.cachecontrol.annotations.LastModified;
6 import de.halbekunst.juplo.cachecontrol.annotations.ETag;
7 import java.lang.annotation.Annotation;
8 import java.lang.reflect.InvocationTargetException;
9 import java.lang.reflect.Method;
10 import java.util.Date;
11 import java.util.Map;
12 import java.util.Map.Entry;
13 import java.util.TreeMap;
14 import javax.servlet.http.HttpServletRequest;
15 import javax.servlet.http.HttpServletResponse;
16 import org.slf4j.Logger;
17 import org.slf4j.LoggerFactory;
18
19 /**
20  *
21  * @author kai
22  */
23 public class CacheControl {
24   private final static Logger log = LoggerFactory.getLogger(CacheControl.class);
25
26   public static final String HEADER_DATE = "Date";
27   public static final String HEADER_CACHE_CONTROL = "Cache-Control";
28   public static final String HEADER_LAST_MODIFIED = "Last-Modified";
29   public static final String HEADER_ETAG = "ETag";
30   public static final String HEADER_EXPIRES = "Expires";
31   public static final String HEADER_PRAGMA = "Pragma";
32   public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
33   public static final String HEADER_IF_NONE_MATCH = "If-None-Match";
34
35   private static final ThreadLocal<CacheMethodHandle> tl = new ThreadLocal<CacheMethodHandle>();
36
37   private Integer defaultCacheSeconds;
38   private Long defaultLastModified;
39
40
41   public void init(Object handler) throws Exception {
42     if (CacheControl.tl.get() == null)
43       CacheControl.tl.set(new ReflectionCacheMethodHandle(handler));
44   }
45
46   public boolean decorate(
47       HttpServletRequest request,
48       HttpServletResponse response,
49       Object handler
50       ) throws Exception
51   {
52     try {
53     CacheMethodHandle controller = CacheControl.tl.get();
54
55     /** Doppelte Ausführung verhindern... */
56     if (controller == null) {
57       /** Dekoration wurde bereits durchgeführt! */
58       return true;
59     }
60
61     /**
62      * Alle Antworten (insbesondere auch 304) sollen nach dem {@plainlink
63      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18 RFC 2616,
64      * Abschnitt 14.18} einen Date-Header enthalten
65      */
66     response.setDateHeader(HEADER_DATE, controller.getTimestamp());
67
68     /** Besondere Maßnahmen für besondere HTTP-Status-Codes ?!? */
69     int status = controller.accepts(request);
70     switch (status) {
71       case HttpServletResponse.SC_OK: // 200
72       case HttpServletResponse.SC_NO_CONTENT: // 204
73       case HttpServletResponse.SC_PARTIAL_CONTENT: // 206
74         /** Normale Antwort! Antwort dekorieren... */
75         break;
76       case HttpServletResponse.SC_BAD_REQUEST: // 400
77       case HttpServletResponse.SC_UNAUTHORIZED: // 401
78       case HttpServletResponse.SC_PAYMENT_REQUIRED: // 402
79       case HttpServletResponse.SC_FORBIDDEN: // 403
80       case HttpServletResponse.SC_NOT_FOUND: // 404
81       case HttpServletResponse.SC_METHOD_NOT_ALLOWED: // 405
82       case HttpServletResponse.SC_NOT_ACCEPTABLE: // 406
83       case HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED: // 407
84       case HttpServletResponse.SC_REQUEST_TIMEOUT: // 408
85       case HttpServletResponse.SC_CONFLICT: // 409
86       case HttpServletResponse.SC_GONE: // 410
87       case HttpServletResponse.SC_LENGTH_REQUIRED: // 411
88       case HttpServletResponse.SC_PRECONDITION_FAILED: // 412
89       case HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE: // 413
90       case HttpServletResponse.SC_REQUEST_URI_TOO_LONG: // 414
91       case HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE: // 415
92       case HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE: // 416
93       case HttpServletResponse.SC_INTERNAL_SERVER_ERROR: // 500
94       case HttpServletResponse.SC_NOT_IMPLEMENTED: // 501
95       case HttpServletResponse.SC_SERVICE_UNAVAILABLE: // 503
96       case HttpServletResponse.SC_HTTP_VERSION_NOT_SUPPORTED: // 505
97         /**
98          * Ein Fehlercode kann stellvertretend für den Handler gesendet werden,
99          * da im Fehlerfall eh keine weiteren Daten ausgegeben werden!
100          */
101         response.sendError(status);
102         return true;
103       default:
104         /**
105          * Es ist nicht klar, was der Handler noch machen wird/muss:
106          * Antwort nicht dekorieren und Kontroller an den Handler übergeben...
107          */
108         return false;
109     }
110
111     String url = null;
112     if (log.isDebugEnabled()) {
113       if (request.getQueryString() == null) {
114         url = request.getRequestURI();
115       }
116       else {
117         StringBuilder builder = new StringBuilder();
118         builder.append(request.getRequestURI());
119         builder.append('?');
120         builder.append(request.getQueryString());
121         url = builder.toString();
122       }
123     }
124
125     int cacheSeconds = controller.getCacheSeconds(request);
126     if (cacheSeconds < 1) {
127       log.debug("{}: caching disabled!", url);
128       response.setDateHeader(HEADER_DATE, controller.getTimestamp());
129       response.setDateHeader(HEADER_EXPIRES, 0);
130       response.addHeader(HEADER_PRAGMA, "no-cache");
131       response.addHeader(HEADER_CACHE_CONTROL, "private");
132       response.addHeader(HEADER_CACHE_CONTROL, "no-cache");
133       response.addHeader(HEADER_CACHE_CONTROL, "no-store");
134       response.addHeader(HEADER_CACHE_CONTROL, "max-age=0");
135       response.addHeader(HEADER_CACHE_CONTROL, "s-max-age=0");
136       return true;
137     }
138
139     long ifModifiedSince = -1;
140     try {
141       ifModifiedSince = request.getDateHeader(HEADER_IF_MODIFIED_SINCE);
142     }
143     catch (Exception e) {
144       log.error("Exception while fetching If-Modified-Since: {}", e);
145     }
146
147     long lastModified = controller.getLastModified(request);
148
149     /**
150      * Sicherstellen, dass der Wert keine Millisekunden enthält, da die
151      * Zeitangabe aus dem Modified-Since-Header keine Millisekunden enthalten
152      * kann und der Test unten dann stets fehlschlagen würde!
153      */
154     lastModified = lastModified - (lastModified % 1000);
155
156     String ifNoneMatch = request.getHeader(HEADER_IF_NONE_MATCH);
157     String eTag = controller.getETag(request);
158
159     /**
160      * 304-Antworten sollen nach dem {@plainlink
161      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 RFC
162      * 2616, Abschnitt 10.3.5} einen ETag-Header enthalten, wenn auch die
163      * 200-Antwort einen enthalten hätte.
164      */
165     if (eTag != null)
166       response.setHeader(HEADER_ETAG, eTag);
167
168
169     if (ifModifiedSince >= lastModified && lastModified > 0) {
170       /**
171        * request.getDateHeader liefert die Zeit als long, oder -1, wenn der
172        * Header nicht existiert. D.h., wenn "If-Modified-Since" nicht gesetzt
173        * ist, wird die komplette Seite ausgeliefert.
174        * Der zusätzliche Test, ob lastModified größer 0 ist, ist nötig, um
175        * Fehler auszuschließen, wenn die Implementierung von Cachable
176        * negative Werte für Last-Modified zurückliefert.
177        */
178       if (log.isDebugEnabled())
179         log.debug("{}: Not modified since {}", url, new Date(ifModifiedSince));
180
181       if (ifNoneMatch == null) {
182         /** Neue Anfrage oder HTTP/1.0 Client! */
183         log.debug("{}: ETag nicht gesetzt -> 304", url);
184         response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
185         return false;
186       }
187     }
188
189     if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) {
190       log.debug("{}: ETag {} not changed -> 304 ", url, ifNoneMatch);
191       response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
192       return false;
193     }
194
195
196     log.debug("{}: first up!", url);
197
198     /** HTTP/1.1-Caching-Header richtig setzen!! */
199     response.setDateHeader(HEADER_LAST_MODIFIED, lastModified);
200
201     /** Cache-Control für HTTP/1.1-Clients generieren */
202     Map<String, String> cacheControl = new TreeMap<String, String>();
203
204     /**
205      * Wenn eins JSESSIONID in der URL enthalten ist, darf die Anfrage nur vom
206      * Browser gecached werden!
207      */
208     if (request.isRequestedSessionIdFromURL()) {
209       cacheControl.put("private", null);
210     }
211     else {
212       /**
213        * Hier muss nicht geprüft werden, ob cacheSeconds > 0 gilt, da in diesem
214        * Fall oben bereits No-Cache-Header generiert und <code>false</code>
215        * zurückgeliefert werden!
216        *
217        * Den Wert als <code>max-age</code> zu den Schlüssel-Wert-Paaren für den
218        * <code>Cache-Control</code>-Header hinzufügen und einen entsprechenden
219        * <code>Expires</code>-Header für HTTP/1.0-Clients setzen.
220        */
221       cacheControl.put("max-age", Integer.toString(cacheSeconds));
222       response.setDateHeader(HEADER_EXPIRES, (controller.getTimestamp() + (long) cacheSeconds * 1000));
223     }
224
225     /** Dem Handler die Gelegenheit geben, den Cache-Controll-Header anzupassen */
226     controller.cacheControl(request, cacheControl);
227
228
229     if (cacheControl.containsKey("private")) {
230       /**
231        * HTTP/1.0 Caches davon abhalten, die Ressource zu cachen (vgl.: RFC
232        * 2616, {@plainlink
233        * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
234        * Abschnitt 14.9.3} und {@plainlink
235        * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.32
236        * Abschnitt 14.32})
237        */
238       response.setDateHeader(HEADER_EXPIRES, 0l);
239       response.addHeader(HEADER_PRAGMA, "no-cache");
240     }
241
242     StringBuilder builder = new StringBuilder();
243     for (Entry<String, String> entry : cacheControl.entrySet()) {
244       builder.setLength(0);
245       builder.append(entry.getKey());
246       if (entry.getValue() != null) {
247         builder.append('=');
248         builder.append(entry.getValue());
249       }
250       response.addHeader(HEADER_CACHE_CONTROL, builder.toString());
251     }
252
253     return true;
254     }
255     finally {
256       /**
257        * Thread-Locale-Variable zurücksetzen, damit
258        * 1.) ein doppelter Aufruf dieser Methode pro Request erkannt werden kann
259        * 2.) der nächste Request nicht mit dem selben Handle weiterarbeitet
260        */
261       CacheControl.tl.set(null);
262     }
263   }
264
265   public void release() {
266     CacheControl.tl.set(null);
267   }
268
269
270   interface CacheMethodHandle {
271     long getTimestamp();
272     int accepts(HttpServletRequest request) throws Exception;
273     int getCacheSeconds(HttpServletRequest request) throws Exception;
274     long getLastModified(HttpServletRequest request) throws Exception;
275     String getETag(HttpServletRequest request) throws Exception;
276     void cacheControl(HttpServletRequest request, Map<String, String> cacheControlMap) throws Exception;
277   }
278
279   class DefaultCacheMethodHandle implements CacheMethodHandle {
280
281     long now = System.currentTimeMillis();
282     Integer cacheSeconds;
283     Long lastModified;
284     String eTag;
285
286
287     DefaultCacheMethodHandle() {
288       this.cacheSeconds = CacheControl.this.defaultCacheSeconds;
289       this.lastModified = CacheControl.this.defaultLastModified;
290       this.eTag = null;
291     }
292
293
294     @Override
295     public long getTimestamp() {
296       return now;
297     }
298
299     @Override
300     public int accepts(HttpServletRequest request) {
301       return HttpServletResponse.SC_OK;
302     }
303
304     @Override
305     public int getCacheSeconds(HttpServletRequest request) {
306       return cacheSeconds;
307     }
308
309     @Override
310     public long getLastModified(HttpServletRequest request) {
311       return lastModified;
312     }
313
314     @Override
315     public String getETag(HttpServletRequest request) {
316       return eTag;
317     }
318
319     @Override
320     public void cacheControl(HttpServletRequest request, Map<String, String> cacheControlMap) {
321     }
322   }
323
324   class ReflectionCacheMethodHandle implements CacheMethodHandle {
325
326     private Object handler;
327     private DefaultCacheMethodHandle defaults = new DefaultCacheMethodHandle();
328     private Method accepts, cacheSeconds, lastModified, eTag, cacheControl;
329     private boolean isAcceptsDefined, isCacheSecondsDefined, isLastModifiedDefined, isETagDefined, isCacheControlDefined;
330
331
332     ReflectionCacheMethodHandle(Object handler) throws NoSuchMethodException {
333       this.handler = handler;
334       /** Class-Level-Annotations auslesen */
335       for (Annotation annotation : handler.getClass().getAnnotations()) {
336         if (annotation.annotationType().equals(CacheSeconds.class)) {
337           defaults.cacheSeconds = ((CacheSeconds)annotation).value();
338           isCacheSecondsDefined = true;
339           continue;
340         }
341         if (annotation.annotationType().equals(LastModified.class)) {
342           defaults.lastModified = ((LastModified)annotation).value();
343           if (defaults.lastModified < 1) {
344             /**
345              * Ein Last-Modified-Header wurde angefordert, aber es wurde kein
346              * statischer Wert spezifiziert:
347              * globalen statischen Default-Wert benutzen!
348              */
349             defaults.lastModified = defaultLastModified;
350           }
351           isLastModifiedDefined = true;
352           continue;
353         }
354         if (annotation.annotationType().equals(ETag.class)) {
355           defaults.eTag = ((ETag)annotation).value();
356           isETagDefined = true;
357           continue;
358         }
359       }
360
361       /** Method-Level-Annotations auslesen */
362       for (Method method : handler.getClass().getMethods()) {
363         for (Annotation annotation : method.getAnnotations()) {
364           if (annotation.annotationType().equals(Accepts.class)) {
365             if (isAcceptsDefined)
366               throw new IllegalArgumentException("Die Annotation @Accept wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
367             accepts = method;
368             isAcceptsDefined = true;
369             continue;
370           }
371           if (annotation.annotationType().equals(CacheSeconds.class)) {
372             if (isCacheSecondsDefined)
373               throw new IllegalArgumentException("Die Annotation @CacheSeconds wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
374             cacheSeconds = method;
375             isCacheSecondsDefined = true;
376             continue;
377           }
378           if (annotation.annotationType().equals(LastModified.class)) {
379             if (isLastModifiedDefined)
380               throw new IllegalArgumentException("Die Annotation @LastModified wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
381             lastModified = method;
382             isLastModifiedDefined = true;
383             continue;
384           }
385           if (annotation.annotationType().equals(ETag.class)) {
386             if (isETagDefined)
387               throw new IllegalArgumentException("Die Annotation @ETag wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
388             eTag = method;
389             isETagDefined = true;
390             continue;
391           }
392           if (annotation.annotationType().equals(de.halbekunst.juplo.cachecontrol.annotations.CacheControl.class)) {
393             if (isCacheControlDefined)
394               throw new IllegalArgumentException("Die Annotation @CacheControl wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
395             cacheControl = method;
396             isCacheControlDefined = true;
397             continue;
398           }
399         }
400       }
401     }
402
403
404     @Override
405     public long getTimestamp() {
406       return defaults.now;
407     }
408
409     @Override
410     public int accepts(HttpServletRequest request)
411         throws IllegalAccessException,
412                IllegalArgumentException,
413                InvocationTargetException
414     {
415       if (accepts == null)
416         return defaults.accepts(request);
417       else
418         return (Integer)accepts.invoke(handler, request);
419     }
420
421     @Override
422     public int getCacheSeconds(HttpServletRequest request)
423         throws IllegalAccessException,
424                IllegalArgumentException,
425                InvocationTargetException
426     {
427       if (cacheSeconds == null)
428         return defaults.getCacheSeconds(request);
429       else
430         return (Integer)cacheSeconds.invoke(handler, request);
431     }
432
433     @Override
434     public long getLastModified(HttpServletRequest request)
435         throws IllegalAccessException,
436                IllegalArgumentException,
437                InvocationTargetException
438     {
439       if (lastModified == null)
440         return defaults.getLastModified(request);
441       else
442         return (Long)lastModified.invoke(handler, request);
443     }
444
445     @Override
446     public String getETag(HttpServletRequest request)
447         throws IllegalAccessException,
448                IllegalArgumentException,
449                InvocationTargetException
450     {
451       if (eTag == null)
452         return defaults.getETag(request);
453       else
454         return (String)eTag.invoke(handler, request);
455     }
456
457     @Override
458     public void cacheControl(
459         HttpServletRequest request,
460         Map<String, String> cacheControlMap
461         )
462         throws IllegalAccessException,
463                IllegalArgumentException,
464                InvocationTargetException
465     {
466       if (cacheControl == null)
467         defaults.cacheControl(request, cacheControlMap);
468       else
469         cacheControl.invoke(handler, request, cacheControlMap);
470     }
471   }
472
473
474   public void setDefaultCacheSeconds(Integer defaultCacheSeconds) {
475     this.defaultCacheSeconds = defaultCacheSeconds;
476   }
477
478   public void setDefaultLastModified(Long defaultLastModified) {
479     this.defaultLastModified = defaultLastModified;
480   }
481 }