1 package de.halbekunst.juplo.cachecontrol;
3 import de.halbekunst.juplo.cachecontrol.annotations.CacheSeconds;
4 import de.halbekunst.juplo.cachecontrol.annotations.Accepts;
5 import de.halbekunst.juplo.cachecontrol.annotations.AdditionalHeaders;
6 import de.halbekunst.juplo.cachecontrol.annotations.LastModified;
7 import de.halbekunst.juplo.cachecontrol.annotations.ETag;
8 import java.lang.annotation.Annotation;
9 import java.lang.reflect.Method;
10 import java.util.Date;
11 import java.util.HashMap;
13 import java.util.Map.Entry;
14 import java.util.TreeMap;
15 import javax.servlet.http.HttpServletRequest;
16 import javax.servlet.http.HttpServletResponse;
17 import org.slf4j.Logger;
18 import org.slf4j.LoggerFactory;
19 import org.springframework.beans.factory.annotation.Autowired;
20 import org.springframework.beans.factory.annotation.Qualifier;
21 import org.springframework.stereotype.Component;
28 public class CacheControl {
29 private final static Logger log = LoggerFactory.getLogger(CacheControl.class);
31 private static final ThreadLocal<CacheMethodHandle> tl = new ThreadLocal<CacheMethodHandle>();
33 @Autowired @Qualifier("cacheSeconds") private Integer defaultCacheSeconds;
34 @Autowired @Qualifier("lastModified") private Long defaultLastModified;
37 public void init(CacheMethodHandle handle) {
38 CacheControl.tl.set(handle);
41 public void init(Object handler) throws NoSuchMethodException {
42 CacheControl.tl.set(new ReflectionCacheMethodHandle(handler));
45 public boolean decorate(
46 HttpServletRequest request,
47 HttpServletResponse response,
52 CacheMethodHandle controller = CacheControl.tl.get();
54 /** Doppelte Ausführung verhindern... */
55 if (controller == null) {
56 /** Dekoration wurde bereits durchgeführt! */
61 * Alle Antworten (insbesondere auch 304) sollen nach dem {@plainlink
62 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18 RFC 2616,
63 * Abschnitt 14.18} einen Date-Header enthalten
65 response.setDateHeader(Headers.HEADER_DATE, controller.getTimestamp());
67 /** Besondere Maßnahmen für besondere HTTP-Status-Codes ?!? */
68 int status = controller.accepts(request);
70 case HttpServletResponse.SC_OK: // 200
71 case HttpServletResponse.SC_NO_CONTENT: // 204
72 case HttpServletResponse.SC_PARTIAL_CONTENT: // 206
73 /** Normale Antwort! Antwort dekorieren... */
75 case HttpServletResponse.SC_BAD_REQUEST: // 400
76 case HttpServletResponse.SC_UNAUTHORIZED: // 401
77 case HttpServletResponse.SC_PAYMENT_REQUIRED: // 402
78 case HttpServletResponse.SC_FORBIDDEN: // 403
79 case HttpServletResponse.SC_NOT_FOUND: // 404
80 case HttpServletResponse.SC_METHOD_NOT_ALLOWED: // 405
81 case HttpServletResponse.SC_NOT_ACCEPTABLE: // 406
82 case HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED: // 407
83 case HttpServletResponse.SC_REQUEST_TIMEOUT: // 408
84 case HttpServletResponse.SC_CONFLICT: // 409
85 case HttpServletResponse.SC_GONE: // 410
86 case HttpServletResponse.SC_LENGTH_REQUIRED: // 411
87 case HttpServletResponse.SC_PRECONDITION_FAILED: // 412
88 case HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE: // 413
89 case HttpServletResponse.SC_REQUEST_URI_TOO_LONG: // 414
90 case HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE: // 415
91 case HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE: // 416
92 case HttpServletResponse.SC_INTERNAL_SERVER_ERROR: // 500
93 case HttpServletResponse.SC_NOT_IMPLEMENTED: // 501
94 case HttpServletResponse.SC_SERVICE_UNAVAILABLE: // 503
95 case HttpServletResponse.SC_HTTP_VERSION_NOT_SUPPORTED: // 505
99 * Es ist nicht klar, was der Handler noch machen wird/muss:
100 * Antwort nicht dekorieren und Kontroller an den Handler übergeben...
105 Map<String,String> headers = controller.getAdditionalHeaders(request);
106 for (String name : headers.keySet())
107 response.addHeader(name, headers.get(name));
110 if (log.isDebugEnabled()) {
111 if (request.getQueryString() == null) {
112 url = request.getRequestURI();
115 StringBuilder builder = new StringBuilder();
116 builder.append(request.getRequestURI());
118 builder.append(request.getQueryString());
119 url = builder.toString();
123 int cacheSeconds = controller.getCacheSeconds(request);
124 if (cacheSeconds < 1) {
125 log.debug("{}: caching disabled!", url);
126 response.setDateHeader(Headers.HEADER_DATE, controller.getTimestamp());
127 response.setDateHeader(Headers.HEADER_EXPIRES, 0);
128 response.addHeader(Headers.HEADER_PRAGMA, "no-cache");
129 response.addHeader(Headers.HEADER_CACHE_CONTROL, "private");
130 response.addHeader(Headers.HEADER_CACHE_CONTROL, "no-cache");
131 response.addHeader(Headers.HEADER_CACHE_CONTROL, "no-store");
132 response.addHeader(Headers.HEADER_CACHE_CONTROL, "max-age=0");
133 response.addHeader(Headers.HEADER_CACHE_CONTROL, "s-max-age=0");
137 long ifModifiedSince = -1;
139 ifModifiedSince = request.getDateHeader(Headers.HEADER_IF_MODIFIED_SINCE);
141 catch (Exception e) {
142 log.error("Exception while fetching If-Modified-Since: {}", e);
145 long lastModified = controller.getLastModified(request);
148 * Sicherstellen, dass der Wert keine Millisekunden enthält, da die
149 * Zeitangabe aus dem Modified-Since-Header keine Millisekunden enthalten
150 * kann und der Test unten dann stets fehlschlagen würde!
152 lastModified = lastModified - (lastModified % 1000);
154 String ifNoneMatch = request.getHeader(Headers.HEADER_IF_NONE_MATCH);
155 String eTag = controller.getETag(request);
158 * 304-Antworten sollen nach dem {@plainlink
159 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 RFC
160 * 2616, Abschnitt 10.3.5} einen ETag-Header enthalten, wenn auch die
161 * 200-Antwort einen enthalten hätte.
164 StringBuilder builder = new StringBuilder();
165 if (controller.isETagWeak())
166 builder.append("W/");
168 builder.append(eTag);
170 response.setHeader(Headers.HEADER_ETAG, builder.toString());
174 if (ifModifiedSince >= lastModified && lastModified > 0) {
176 * request.getDateHeader liefert die Zeit als long, oder -1, wenn der
177 * Header nicht existiert. D.h., wenn "If-Modified-Since" nicht gesetzt
178 * ist, wird die komplette Seite ausgeliefert.
179 * Der zusätzliche Test, ob lastModified größer 0 ist, ist nötig, um
180 * Fehler auszuschließen, wenn die Implementierung von Cachable
181 * negative Werte für Last-Modified zurückliefert.
183 if (log.isDebugEnabled())
184 log.debug("{}: Not modified since {}", url, new Date(ifModifiedSince));
186 if (ifNoneMatch == null) {
187 /** Neue Anfrage oder HTTP/1.0 Client! */
188 log.debug("{}: ETag nicht gesetzt -> 304", url);
189 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
194 if (ifNoneMatch != null) {
195 boolean weak = false;
196 if (ifNoneMatch.startsWith("W/")) {
198 ifNoneMatch = ifNoneMatch.substring(3, ifNoneMatch.length() - 1);
201 ifNoneMatch = ifNoneMatch.substring(1, ifNoneMatch.length() - 1);
204 if (!weak || (request.getMethod().equals("GET") && request.getHeader(Headers.HEADER_RANGE) == null)) {
206 * Die Gleichheit gilt nur, wenn die ETag's der Anfrage _und_ der
207 * Antwort stark sind (starke Gleichheit!), oder wenn die Antwort nur
208 * schwache Gleichheit fordert...
210 if (ifNoneMatch.equals(eTag) && (controller.isETagWeak() || !weak)) {
211 log.debug("{}: ETag {} not changed -> 304 ", url, ifNoneMatch);
212 response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
217 log.warn("{}: ignoring weak ETag W/\"{}\", because the request was no GET-request or the Range-Header was present!", url, ifNoneMatch);
222 log.debug("{}: first up!", url);
224 /** HTTP/1.1-Caching-Header richtig setzen!! */
225 response.setDateHeader(Headers.HEADER_LAST_MODIFIED, lastModified);
227 /** Cache-Control für HTTP/1.1-Clients generieren */
228 Map<String, String> cacheControl = new TreeMap<String, String>();
231 * Wenn eins JSESSIONID in der URL enthalten ist, darf die Anfrage nur vom
232 * Browser gecached werden!
234 if (request.isRequestedSessionIdFromURL()) {
235 cacheControl.put("private", null);
239 * Hier muss nicht geprüft werden, ob cacheSeconds > 0 gilt, da in diesem
240 * Fall oben bereits No-Cache-Header generiert und <code>false</code>
241 * zurückgeliefert werden!
243 * Den Wert als <code>max-age</code> zu den Schlüssel-Wert-Paaren für den
244 * <code>Cache-Control</code>-Header hinzufügen und einen entsprechenden
245 * <code>Expires</code>-Header für HTTP/1.0-Clients setzen.
247 cacheControl.put("max-age", Integer.toString(cacheSeconds));
248 response.setDateHeader(Headers.HEADER_EXPIRES, (controller.getTimestamp() + (long) cacheSeconds * 1000));
251 /** Dem Handler die Gelegenheit geben, den Cache-Controll-Header anzupassen */
252 controller.cacheControl(request, cacheControl);
255 if (cacheControl.containsKey("private")) {
257 * HTTP/1.0 Caches davon abhalten, die Ressource zu cachen (vgl.: RFC
259 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
260 * Abschnitt 14.9.3} und {@plainlink
261 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.32
264 response.setDateHeader(Headers.HEADER_EXPIRES, 0l);
265 response.addHeader(Headers.HEADER_PRAGMA, "no-cache");
268 StringBuilder builder = new StringBuilder();
269 for (Entry<String, String> entry : cacheControl.entrySet()) {
270 builder.setLength(0);
271 builder.append(entry.getKey());
272 if (entry.getValue() != null) {
274 builder.append(entry.getValue());
276 response.addHeader(Headers.HEADER_CACHE_CONTROL, builder.toString());
283 * Thread-Locale-Variable zurücksetzen, damit
284 * 1.) ein doppelter Aufruf dieser Methode pro Request erkannt werden kann
285 * 2.) der nächste Request nicht mit dem selben Handle weiterarbeitet
287 CacheControl.tl.set(null);
291 public void release() {
292 CacheControl.tl.set(null);
296 public interface CacheMethodHandle {
298 int accepts(HttpServletRequest request);
299 int getCacheSeconds(HttpServletRequest request);
300 long getLastModified(HttpServletRequest request);
301 String getETag(HttpServletRequest request);
302 boolean isETagWeak();
303 void cacheControl(HttpServletRequest request, Map<String, String> cacheControlMap);
304 Map<String,String> getAdditionalHeaders(HttpServletRequest request);
308 class ReflectionCacheMethodHandle implements CacheMethodHandle {
310 private Object handler;
311 private long now = System.currentTimeMillis();
312 private Integer cacheSeconds;
313 private Long lastModified;
315 private Map<String,String> additionalHeaders;
316 private Method acceptsMethod;
317 private Method cacheSecondsMethod;
318 private Method lastModifiedMethod;
319 private Method eTagMethod;
320 private Method cacheControlMethod;
321 private Method additionalHeadersMethod;
322 private boolean isAcceptsMethodDefined;
323 private boolean isCacheSecondsMethodDefined;
324 private boolean isLastModifiedMethodDefined;
325 private boolean isETagMethodDefined;
326 private boolean isCacheControlMethodDefined;
327 private boolean isAdditionalHeadersMethodDefined;
328 private boolean weak;
331 ReflectionCacheMethodHandle(Object handler) throws NoSuchMethodException {
333 this.handler = handler;
335 cacheSeconds = CacheControl.this.defaultCacheSeconds;
336 lastModified = CacheControl.this.defaultLastModified;
339 /** Class-Level-Annotations auslesen */
340 for (Annotation annotation : handler.getClass().getAnnotations()) {
341 if (annotation.annotationType().equals(CacheSeconds.class)) {
342 cacheSeconds = ((CacheSeconds)annotation).value();
343 isCacheSecondsMethodDefined = true;
346 if (annotation.annotationType().equals(LastModified.class)) {
347 lastModified = ((LastModified)annotation).value();
348 if (lastModified < 1) {
350 * Ein Last-Modified-Header wurde angefordert, aber es wurde kein
351 * statischer Wert spezifiziert:
352 * globalen statischen Default-Wert benutzen!
354 lastModified = defaultLastModified;
356 isLastModifiedMethodDefined = true;
359 if (annotation.annotationType().equals(ETag.class)) {
360 ETag eTagAnnotation = (ETag)annotation;
361 eTag = eTagAnnotation.value();
362 weak = eTagAnnotation.weak();
363 isETagMethodDefined = true;
366 if (annotation.annotationType().equals(AdditionalHeaders.class)) {
367 AdditionalHeaders additionalHeadersAnnotation = (AdditionalHeaders)annotation;
368 additionalHeaders = new HashMap<String,String>();
369 for (String header : additionalHeadersAnnotation.value()) {
370 int i = header.indexOf(':');
372 log.error("invalid header: [{}]", header);
375 String name = header.substring(0,i).trim();
376 String value = header.substring(i+1,header.length()).trim();
377 additionalHeaders.put(name, value);
380 isAdditionalHeadersMethodDefined = true;
385 /** Method-Level-Annotations auslesen */
386 for (Method method : handler.getClass().getMethods()) {
387 for (Annotation annotation : method.getAnnotations()) {
388 if (annotation.annotationType().equals(Accepts.class)) {
389 if (isAcceptsMethodDefined)
390 throw new IllegalArgumentException("Die Annotation @Accept wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
391 acceptsMethod = method;
392 isAcceptsMethodDefined = true;
395 if (annotation.annotationType().equals(CacheSeconds.class)) {
396 if (isCacheSecondsMethodDefined)
397 throw new IllegalArgumentException("Die Annotation @CacheSeconds wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
398 cacheSecondsMethod = method;
399 isCacheSecondsMethodDefined = true;
402 if (annotation.annotationType().equals(LastModified.class)) {
403 if (isLastModifiedMethodDefined)
404 throw new IllegalArgumentException("Die Annotation @LastModified wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
405 lastModifiedMethod = method;
406 isLastModifiedMethodDefined = true;
409 if (annotation.annotationType().equals(ETag.class)) {
410 if (isETagMethodDefined)
411 throw new IllegalArgumentException("Die Annotation @ETag wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
413 weak = ((ETag)annotation).weak();
414 isETagMethodDefined = true;
417 if (annotation.annotationType().equals(de.halbekunst.juplo.cachecontrol.annotations.CacheControl.class)) {
418 if (isCacheControlMethodDefined)
419 throw new IllegalArgumentException("Die Annotation @CacheControl wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
420 cacheControlMethod = method;
421 isCacheControlMethodDefined = true;
424 if (annotation.annotationType().equals(AdditionalHeaders.class)) {
425 if (isAdditionalHeadersMethodDefined)
426 throw new IllegalArgumentException("Die Annotation @AdditionalHeaders wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
427 additionalHeadersMethod = method;
428 isAdditionalHeadersMethodDefined = true;
434 if (!isAdditionalHeadersMethodDefined)
435 additionalHeaders = new HashMap<String,String>();
440 public long getTimestamp() {
445 public int accepts(HttpServletRequest request) throws IllegalArgumentException {
446 if (acceptsMethod == null) {
447 return HttpServletResponse.SC_OK;
451 return (Integer)acceptsMethod.invoke(handler, request);
453 catch (Exception e) {
454 throw new IllegalArgumentException(e);
460 public int getCacheSeconds(HttpServletRequest request) throws IllegalArgumentException {
461 if (cacheSecondsMethod == null) {
466 return (Integer)cacheSecondsMethod.invoke(handler, request);
468 catch (Exception e) {
469 throw new IllegalArgumentException(e);
475 public long getLastModified(HttpServletRequest request) throws IllegalArgumentException {
476 if (lastModifiedMethod == null) {
481 return (Long)lastModifiedMethod.invoke(handler, request);
483 catch (Exception e) {
484 throw new IllegalArgumentException(e);
490 public String getETag(HttpServletRequest request) throws IllegalArgumentException {
491 if (eTagMethod == null) {
496 return (String)eTagMethod.invoke(handler, request);
498 catch (Exception e) {
499 throw new IllegalArgumentException(e);
505 public boolean isETagWeak() {
510 public void cacheControl(
511 HttpServletRequest request,
512 Map<String, String> cacheControlMap
514 throws IllegalArgumentException
516 if (cacheControlMethod != null) {
518 cacheControlMethod.invoke(handler, request, cacheControlMap);
520 catch (Exception e) {
521 throw new IllegalArgumentException(e);
527 public Map<String,String> getAdditionalHeaders(HttpServletRequest request) throws IllegalArgumentException {
528 if (additionalHeadersMethod == null) {
529 return additionalHeaders;
533 return (Map<String,String>)additionalHeadersMethod.invoke(handler, request);
535 catch (Exception e) {
536 throw new IllegalArgumentException(e);