Überflüssige Hilfsklasse DefaultCacheMethodHandle ausgebaut
[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
280   class ReflectionCacheMethodHandle implements CacheMethodHandle {
281
282     private Object handler;
283     private long now = System.currentTimeMillis();
284     private Integer cacheSeconds;
285     private Long lastModified;
286     private String eTag;
287     private Method acceptsMethod;
288     private Method cacheSecondsMethod;
289     private Method lastModifiedMethod;
290     private Method eTagMethod;
291     private Method cacheControlMethod;
292     private boolean isAcceptsMethodDefined;
293     private boolean isCacheSecondsMethodDefined;
294     private boolean isLastModifiedMethodDefined;
295     private boolean isETagMethodDefined;
296     private boolean isCacheControlMethodDefined;
297
298
299     ReflectionCacheMethodHandle(Object handler) throws NoSuchMethodException {
300
301       this.handler = handler;
302
303       cacheSeconds = CacheControl.this.defaultCacheSeconds;
304       lastModified = CacheControl.this.defaultLastModified;
305       eTag = null;
306
307       /** Class-Level-Annotations auslesen */
308       for (Annotation annotation : handler.getClass().getAnnotations()) {
309         if (annotation.annotationType().equals(CacheSeconds.class)) {
310           cacheSeconds = ((CacheSeconds)annotation).value();
311           isCacheSecondsMethodDefined = true;
312           continue;
313         }
314         if (annotation.annotationType().equals(LastModified.class)) {
315           lastModified = ((LastModified)annotation).value();
316           if (lastModified < 1) {
317             /**
318              * Ein Last-Modified-Header wurde angefordert, aber es wurde kein
319              * statischer Wert spezifiziert:
320              * globalen statischen Default-Wert benutzen!
321              */
322             lastModified = defaultLastModified;
323           }
324           isLastModifiedMethodDefined = true;
325           continue;
326         }
327         if (annotation.annotationType().equals(ETag.class)) {
328           eTag = ((ETag)annotation).value();
329           isETagMethodDefined = true;
330           continue;
331         }
332       }
333
334       /** Method-Level-Annotations auslesen */
335       for (Method method : handler.getClass().getMethods()) {
336         for (Annotation annotation : method.getAnnotations()) {
337           if (annotation.annotationType().equals(Accepts.class)) {
338             if (isAcceptsMethodDefined)
339               throw new IllegalArgumentException("Die Annotation @Accept wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
340             acceptsMethod = method;
341             isAcceptsMethodDefined = true;
342             continue;
343           }
344           if (annotation.annotationType().equals(CacheSeconds.class)) {
345             if (isCacheSecondsMethodDefined)
346               throw new IllegalArgumentException("Die Annotation @CacheSeconds wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
347             cacheSecondsMethod = method;
348             isCacheSecondsMethodDefined = true;
349             continue;
350           }
351           if (annotation.annotationType().equals(LastModified.class)) {
352             if (isLastModifiedMethodDefined)
353               throw new IllegalArgumentException("Die Annotation @LastModified wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
354             lastModifiedMethod = method;
355             isLastModifiedMethodDefined = true;
356             continue;
357           }
358           if (annotation.annotationType().equals(ETag.class)) {
359             if (isETagMethodDefined)
360               throw new IllegalArgumentException("Die Annotation @ETag wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
361             eTagMethod = method;
362             isETagMethodDefined = true;
363             continue;
364           }
365           if (annotation.annotationType().equals(de.halbekunst.juplo.cachecontrol.annotations.CacheControl.class)) {
366             if (isCacheControlMethodDefined)
367               throw new IllegalArgumentException("Die Annotation @CacheControl wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
368             cacheControlMethod = method;
369             isCacheControlMethodDefined = true;
370             continue;
371           }
372         }
373       }
374     }
375
376
377     @Override
378     public long getTimestamp() {
379       return now;
380     }
381
382     @Override
383     public int accepts(HttpServletRequest request)
384         throws IllegalAccessException,
385                IllegalArgumentException,
386                InvocationTargetException
387     {
388       if (acceptsMethod == null)
389         return HttpServletResponse.SC_OK;
390       else
391         return (Integer)acceptsMethod.invoke(handler, request);
392     }
393
394     @Override
395     public int getCacheSeconds(HttpServletRequest request)
396         throws IllegalAccessException,
397                IllegalArgumentException,
398                InvocationTargetException
399     {
400       if (cacheSecondsMethod == null)
401         return cacheSeconds;
402       else
403         return (Integer)cacheSecondsMethod.invoke(handler, request);
404     }
405
406     @Override
407     public long getLastModified(HttpServletRequest request)
408         throws IllegalAccessException,
409                IllegalArgumentException,
410                InvocationTargetException
411     {
412       if (lastModifiedMethod == null)
413         return lastModified;
414       else
415         return (Long)lastModifiedMethod.invoke(handler, request);
416     }
417
418     @Override
419     public String getETag(HttpServletRequest request)
420         throws IllegalAccessException,
421                IllegalArgumentException,
422                InvocationTargetException
423     {
424       if (eTagMethod == null)
425         return eTag;
426       else
427         return (String)eTagMethod.invoke(handler, request);
428     }
429
430     @Override
431     public void cacheControl(
432         HttpServletRequest request,
433         Map<String, String> cacheControlMap
434         )
435         throws IllegalAccessException,
436                IllegalArgumentException,
437                InvocationTargetException
438     {
439       if (cacheControlMethod != null)
440         cacheControlMethod.invoke(handler, request, cacheControlMap);
441     }
442   }
443
444
445   public void setDefaultCacheSeconds(Integer defaultCacheSeconds) {
446     this.defaultCacheSeconds = defaultCacheSeconds;
447   }
448
449   public void setDefaultLastModified(Long defaultLastModified) {
450     this.defaultLastModified = defaultLastModified;
451   }
452 }