From: Kai Moritz Date: Sat, 12 Nov 2011 16:58:38 +0000 (+0100) Subject: CacheControl so umgebaut, dass es sich über Annotationen einbinden lässt X-Git-Url: https://juplo.de/gitweb/?p=percentcodec;a=commitdiff_plain;h=75b55d74f57705c6c5ebee2bcd66c87f3f09089d CacheControl so umgebaut, dass es sich über Annotationen einbinden lässt --- diff --git a/cachecontrol/pom.xml b/cachecontrol/pom.xml index 0db7fde2..fb5f79e8 100644 --- a/cachecontrol/pom.xml +++ b/cachecontrol/pom.xml @@ -7,7 +7,7 @@ de.halbekunst juplo - 1.0.1 + 2.0-SNAPSHOT ${pom.parent.artifactId}-cachecontrol diff --git a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/CacheControl.java b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/CacheControl.java new file mode 100644 index 00000000..07f4c58e --- /dev/null +++ b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/CacheControl.java @@ -0,0 +1,481 @@ +package de.halbekunst.juplo.cachecontrol; + +import de.halbekunst.juplo.cachecontrol.annotations.CacheSeconds; +import de.halbekunst.juplo.cachecontrol.annotations.Accepts; +import de.halbekunst.juplo.cachecontrol.annotations.LastModified; +import de.halbekunst.juplo.cachecontrol.annotations.ETag; +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Date; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TreeMap; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author kai + */ +public class CacheControl { + private final static Logger log = LoggerFactory.getLogger(CacheControl.class); + + public static final String HEADER_DATE = "Date"; + public static final String HEADER_CACHE_CONTROL = "Cache-Control"; + public static final String HEADER_LAST_MODIFIED = "Last-Modified"; + public static final String HEADER_ETAG = "ETag"; + public static final String HEADER_EXPIRES = "Expires"; + public static final String HEADER_PRAGMA = "Pragma"; + public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; + public static final String HEADER_IF_NONE_MATCH = "If-None-Match"; + + private static final ThreadLocal tl = new ThreadLocal(); + + private Integer defaultCacheSeconds; + private Long defaultLastModified; + + + public void init(Object handler) throws Exception { + if (CacheControl.tl.get() == null) + CacheControl.tl.set(new ReflectionCacheMethodHandle(handler)); + } + + public boolean decorate( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) throws Exception + { + try { + CacheMethodHandle controller = CacheControl.tl.get(); + + /** Doppelte Ausführung verhindern... */ + if (controller == null) { + /** Dekoration wurde bereits durchgeführt! */ + return true; + } + + /** + * Alle Antworten (insbesondere auch 304) sollen nach dem {@plainlink + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18 RFC 2616, + * Abschnitt 14.18} einen Date-Header enthalten + */ + response.setDateHeader(HEADER_DATE, controller.getTimestamp()); + + /** Besondere Maßnahmen für besondere HTTP-Status-Codes ?!? */ + int status = controller.accepts(request); + switch (status) { + case HttpServletResponse.SC_OK: // 200 + case HttpServletResponse.SC_NO_CONTENT: // 204 + case HttpServletResponse.SC_PARTIAL_CONTENT: // 206 + /** Normale Antwort! Antwort dekorieren... */ + break; + case HttpServletResponse.SC_BAD_REQUEST: // 400 + case HttpServletResponse.SC_UNAUTHORIZED: // 401 + case HttpServletResponse.SC_PAYMENT_REQUIRED: // 402 + case HttpServletResponse.SC_FORBIDDEN: // 403 + case HttpServletResponse.SC_NOT_FOUND: // 404 + case HttpServletResponse.SC_METHOD_NOT_ALLOWED: // 405 + case HttpServletResponse.SC_NOT_ACCEPTABLE: // 406 + case HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED: // 407 + case HttpServletResponse.SC_REQUEST_TIMEOUT: // 408 + case HttpServletResponse.SC_CONFLICT: // 409 + case HttpServletResponse.SC_GONE: // 410 + case HttpServletResponse.SC_LENGTH_REQUIRED: // 411 + case HttpServletResponse.SC_PRECONDITION_FAILED: // 412 + case HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE: // 413 + case HttpServletResponse.SC_REQUEST_URI_TOO_LONG: // 414 + case HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE: // 415 + case HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE: // 416 + case HttpServletResponse.SC_INTERNAL_SERVER_ERROR: // 500 + case HttpServletResponse.SC_NOT_IMPLEMENTED: // 501 + case HttpServletResponse.SC_SERVICE_UNAVAILABLE: // 503 + case HttpServletResponse.SC_HTTP_VERSION_NOT_SUPPORTED: // 505 + /** + * Ein Fehlercode kann stellvertretend für den Handler gesendet werden, + * da im Fehlerfall eh keine weiteren Daten ausgegeben werden! + */ + response.sendError(status); + return true; + default: + /** + * Es ist nicht klar, was der Handler noch machen wird/muss: + * Antwort nicht dekorieren und Kontroller an den Handler übergeben... + */ + return false; + } + + String url = null; + if (log.isDebugEnabled()) { + if (request.getQueryString() == null) { + url = request.getRequestURI(); + } + else { + StringBuilder builder = new StringBuilder(); + builder.append(request.getRequestURI()); + builder.append('?'); + builder.append(request.getQueryString()); + url = builder.toString(); + } + } + + int cacheSeconds = controller.getCacheSeconds(request); + if (cacheSeconds < 1) { + log.debug("{}: caching disabled!", url); + response.setDateHeader(HEADER_DATE, controller.getTimestamp()); + response.setDateHeader(HEADER_EXPIRES, 0); + response.addHeader(HEADER_PRAGMA, "no-cache"); + response.addHeader(HEADER_CACHE_CONTROL, "private"); + response.addHeader(HEADER_CACHE_CONTROL, "no-cache"); + response.addHeader(HEADER_CACHE_CONTROL, "no-store"); + response.addHeader(HEADER_CACHE_CONTROL, "max-age=0"); + response.addHeader(HEADER_CACHE_CONTROL, "s-max-age=0"); + return true; + } + + long ifModifiedSince = -1; + try { + ifModifiedSince = request.getDateHeader(HEADER_IF_MODIFIED_SINCE); + } + catch (Exception e) { + log.error("Exception while fetching If-Modified-Since: {}", e); + } + + long lastModified = controller.getLastModified(request); + + /** + * Sicherstellen, dass der Wert keine Millisekunden enthält, da die + * Zeitangabe aus dem Modified-Since-Header keine Millisekunden enthalten + * kann und der Test unten dann stets fehlschlagen würde! + */ + lastModified = lastModified - (lastModified % 1000); + + String ifNoneMatch = request.getHeader(HEADER_IF_NONE_MATCH); + String eTag = controller.getETag(request); + + /** + * 304-Antworten sollen nach dem {@plainlink + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 RFC + * 2616, Abschnitt 10.3.5} einen ETag-Header enthalten, wenn auch die + * 200-Antwort einen enthalten hätte. + */ + if (eTag != null) + response.setHeader(HEADER_ETAG, eTag); + + + if (ifModifiedSince >= lastModified && lastModified > 0) { + /** + * request.getDateHeader liefert die Zeit als long, oder -1, wenn der + * Header nicht existiert. D.h., wenn "If-Modified-Since" nicht gesetzt + * ist, wird die komplette Seite ausgeliefert. + * Der zusätzliche Test, ob lastModified größer 0 ist, ist nötig, um + * Fehler auszuschließen, wenn die Implementierung von Cachable + * negative Werte für Last-Modified zurückliefert. + */ + if (log.isDebugEnabled()) + log.debug("{}: Not modified since {}", url, new Date(ifModifiedSince)); + + if (ifNoneMatch == null) { + /** Neue Anfrage oder HTTP/1.0 Client! */ + log.debug("{}: ETag nicht gesetzt -> 304", url); + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return false; + } + } + + if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) { + log.debug("{}: ETag {} not changed -> 304 ", url, ifNoneMatch); + response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); + return false; + } + + + log.debug("{}: first up!", url); + + /** HTTP/1.1-Caching-Header richtig setzen!! */ + response.setDateHeader(HEADER_LAST_MODIFIED, lastModified); + + /** Cache-Control für HTTP/1.1-Clients generieren */ + Map cacheControl = new TreeMap(); + + /** + * Wenn eins JSESSIONID in der URL enthalten ist, darf die Anfrage nur vom + * Browser gecached werden! + */ + if (request.isRequestedSessionIdFromURL()) { + cacheControl.put("private", null); + } + else { + /** + * Hier muss nicht geprüft werden, ob cacheSeconds > 0 gilt, da in diesem + * Fall oben bereits No-Cache-Header generiert und false + * zurückgeliefert werden! + * + * Den Wert als max-age zu den Schlüssel-Wert-Paaren für den + * Cache-Control-Header hinzufügen und einen entsprechenden + * Expires-Header für HTTP/1.0-Clients setzen. + */ + cacheControl.put("max-age", Integer.toString(cacheSeconds)); + response.setDateHeader(HEADER_EXPIRES, (controller.getTimestamp() + (long) cacheSeconds * 1000)); + } + + /** Dem Handler die Gelegenheit geben, den Cache-Controll-Header anzupassen */ + controller.cacheControl(request, cacheControl); + + + if (cacheControl.containsKey("private")) { + /** + * HTTP/1.0 Caches davon abhalten, die Ressource zu cachen (vgl.: RFC + * 2616, {@plainlink + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3 + * Abschnitt 14.9.3} und {@plainlink + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.32 + * Abschnitt 14.32}) + */ + response.setDateHeader(HEADER_EXPIRES, 0l); + response.addHeader(HEADER_PRAGMA, "no-cache"); + } + + StringBuilder builder = new StringBuilder(); + for (Entry entry : cacheControl.entrySet()) { + builder.setLength(0); + builder.append(entry.getKey()); + if (entry.getValue() != null) { + builder.append('='); + builder.append(entry.getValue()); + } + response.addHeader(HEADER_CACHE_CONTROL, builder.toString()); + } + + return true; + } + finally { + /** + * Thread-Locale-Variable zurücksetzen, damit + * 1.) ein doppelter Aufruf dieser Methode pro Request erkannt werden kann + * 2.) der nächste Request nicht mit dem selben Handle weiterarbeitet + */ + CacheControl.tl.set(null); + } + } + + public void release() { + CacheControl.tl.set(null); + } + + + interface CacheMethodHandle { + long getTimestamp(); + int accepts(HttpServletRequest request) throws Exception; + int getCacheSeconds(HttpServletRequest request) throws Exception; + long getLastModified(HttpServletRequest request) throws Exception; + String getETag(HttpServletRequest request) throws Exception; + void cacheControl(HttpServletRequest request, Map cacheControlMap) throws Exception; + } + + class DefaultCacheMethodHandle implements CacheMethodHandle { + + long now = System.currentTimeMillis(); + Integer cacheSeconds; + Long lastModified; + String eTag; + + + DefaultCacheMethodHandle() { + this.cacheSeconds = CacheControl.this.defaultCacheSeconds; + this.lastModified = CacheControl.this.defaultLastModified; + this.eTag = null; + } + + + @Override + public long getTimestamp() { + return now; + } + + @Override + public int accepts(HttpServletRequest request) { + return HttpServletResponse.SC_OK; + } + + @Override + public int getCacheSeconds(HttpServletRequest request) { + return cacheSeconds; + } + + @Override + public long getLastModified(HttpServletRequest request) { + return lastModified; + } + + @Override + public String getETag(HttpServletRequest request) { + return eTag; + } + + @Override + public void cacheControl(HttpServletRequest request, Map cacheControlMap) { + } + } + + class ReflectionCacheMethodHandle implements CacheMethodHandle { + + private Object handler; + private DefaultCacheMethodHandle defaults = new DefaultCacheMethodHandle(); + private Method accepts, cacheSeconds, lastModified, eTag, cacheControl; + private boolean isAcceptsDefined, isCacheSecondsDefined, isLastModifiedDefined, isETagDefined, isCacheControlDefined; + + + ReflectionCacheMethodHandle(Object handler) throws NoSuchMethodException { + this.handler = handler; + /** Class-Level-Annotations auslesen */ + for (Annotation annotation : handler.getClass().getAnnotations()) { + if (annotation.annotationType().equals(CacheSeconds.class)) { + defaults.cacheSeconds = ((CacheSeconds)annotation).value(); + isCacheSecondsDefined = true; + continue; + } + if (annotation.annotationType().equals(LastModified.class)) { + defaults.lastModified = ((LastModified)annotation).value(); + if (defaults.lastModified < 1) { + /** + * Ein Last-Modified-Header wurde angefordert, aber es wurde kein + * statischer Wert spezifiziert: + * globalen statischen Default-Wert benutzen! + */ + defaults.lastModified = defaultLastModified; + } + isLastModifiedDefined = true; + continue; + } + if (annotation.annotationType().equals(ETag.class)) { + defaults.eTag = ((ETag)annotation).value(); + isETagDefined = true; + continue; + } + } + + /** Method-Level-Annotations auslesen */ + for (Method method : handler.getClass().getMethods()) { + for (Annotation annotation : method.getAnnotations()) { + if (annotation.annotationType().equals(Accepts.class)) { + if (isAcceptsDefined) + throw new IllegalArgumentException("Die Annotation @Accept wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!"); + accepts = method; + isAcceptsDefined = true; + continue; + } + if (annotation.annotationType().equals(CacheSeconds.class)) { + if (isCacheSecondsDefined) + throw new IllegalArgumentException("Die Annotation @CacheSeconds wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!"); + cacheSeconds = method; + isCacheSecondsDefined = true; + continue; + } + if (annotation.annotationType().equals(LastModified.class)) { + if (isLastModifiedDefined) + throw new IllegalArgumentException("Die Annotation @LastModified wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!"); + lastModified = method; + isLastModifiedDefined = true; + continue; + } + if (annotation.annotationType().equals(ETag.class)) { + if (isETagDefined) + throw new IllegalArgumentException("Die Annotation @ETag wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!"); + eTag = method; + isETagDefined = true; + continue; + } + if (annotation.annotationType().equals(de.halbekunst.juplo.cachecontrol.annotations.CacheControl.class)) { + if (isCacheControlDefined) + throw new IllegalArgumentException("Die Annotation @CacheControl wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!"); + cacheControl = method; + isCacheControlDefined = true; + continue; + } + } + } + } + + + @Override + public long getTimestamp() { + return defaults.now; + } + + @Override + public int accepts(HttpServletRequest request) + throws IllegalAccessException, + IllegalArgumentException, + InvocationTargetException + { + if (accepts == null) + return defaults.accepts(request); + else + return (Integer)accepts.invoke(handler, request); + } + + @Override + public int getCacheSeconds(HttpServletRequest request) + throws IllegalAccessException, + IllegalArgumentException, + InvocationTargetException + { + if (cacheSeconds == null) + return defaults.getCacheSeconds(request); + else + return (Integer)cacheSeconds.invoke(handler, request); + } + + @Override + public long getLastModified(HttpServletRequest request) + throws IllegalAccessException, + IllegalArgumentException, + InvocationTargetException + { + if (lastModified == null) + return defaults.getLastModified(request); + else + return (Long)lastModified.invoke(handler, request); + } + + @Override + public String getETag(HttpServletRequest request) + throws IllegalAccessException, + IllegalArgumentException, + InvocationTargetException + { + if (eTag == null) + return defaults.getETag(request); + else + return (String)eTag.invoke(handler, request); + } + + @Override + public void cacheControl( + HttpServletRequest request, + Map cacheControlMap + ) + throws IllegalAccessException, + IllegalArgumentException, + InvocationTargetException + { + if (cacheControl == null) + defaults.cacheControl(request, cacheControlMap); + else + cacheControl.invoke(handler, request, cacheControlMap); + } + } + + + public void setDefaultCacheSeconds(Integer defaultCacheSeconds) { + this.defaultCacheSeconds = defaultCacheSeconds; + } + + public void setDefaultLastModified(Long defaultLastModified) { + this.defaultLastModified = defaultLastModified; + } +} diff --git a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/CacheControlFilter.java b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/CacheControlFilter.java new file mode 100644 index 00000000..549f35ce --- /dev/null +++ b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/CacheControlFilter.java @@ -0,0 +1,209 @@ +package de.halbekunst.juplo.cachecontrol; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Locale; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author kai + */ +public class CacheControlFilter implements Filter { + + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void destroy() { + throw new UnsupportedOperationException("Not supported yet."); + } + + + class HttpServletResponseWrapper implements HttpServletResponse { + + private final HttpServletResponse response; + + + HttpServletResponseWrapper(HttpServletResponse response) { + this.response = response; + } + + + @Override + public void addCookie(Cookie cookie) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean containsHeader(String name) { + return response.containsHeader(name); + } + + @Override + public String encodeURL(String url) { + return response.encodeURL(url); + } + + @Override + public String encodeRedirectURL(String url) { + return response.encodeRedirectURL(url); + } + + @Override + public String encodeUrl(String url) { + return response.encodeUrl(url); + } + + @Override + public String encodeRedirectUrl(String url) { + return response.encodeRedirectUrl(url); + } + + @Override + public void sendError(int sc, String msg) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void sendError(int sc) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void sendRedirect(String location) throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setDateHeader(String name, long date) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void addDateHeader(String name, long date) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setHeader(String name, String value) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void addHeader(String name, String value) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setIntHeader(String name, int value) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void addIntHeader(String name, int value) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setStatus(int sc) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setStatus(int sc, String sm) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String getCharacterEncoding() { + return response.getCharacterEncoding(); + } + + @Override + public String getContentType() { + return response.getContentType(); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public PrintWriter getWriter() throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setCharacterEncoding(String charset) { + response.setCharacterEncoding(charset); + } + + @Override + public void setContentLength(int len) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setContentType(String type) { + response.setContentType(type); + } + + @Override + public void setBufferSize(int size) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public int getBufferSize() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void flushBuffer() throws IOException { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void resetBuffer() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isCommitted() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void reset() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void setLocale(Locale loc) { + response.setLocale(loc); + } + + @Override + public Locale getLocale() { + return response.getLocale(); + } + } +} diff --git a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/CacheControlInterceptor.java b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/CacheControlInterceptor.java index a830e3c6..fe51187f 100644 --- a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/CacheControlInterceptor.java +++ b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/CacheControlInterceptor.java @@ -1,13 +1,11 @@ package de.halbekunst.juplo.cachecontrol; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; +import de.halbekunst.juplo.cachecontrol.annotations.Cacheable; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; @@ -18,184 +16,58 @@ import org.springframework.web.servlet.ModelAndView; public class CacheControlInterceptor implements HandlerInterceptor { private final static Logger log = LoggerFactory.getLogger(CacheControlInterceptor.class); - public static final String HEADER_DATE = "Date"; - public static final String HEADER_CACHE_CONTROL = "Cache-Control"; - public static final String HEADER_LAST_MODIFIED = "Last-Modified"; - public static final String HEADER_ETAG = "ETag"; - public static final String HEADER_EXPIRES = "Expires"; - public static final String HEADER_PRAGMA = "Pragma"; - public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; - public static final String HEADER_IF_NONE_MATCH = "If-None-Match"; + + private CacheControl cacheControl; @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - if (!(handler instanceof Cacheable)) + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) throws Exception + { + Cacheable cacheable = handler.getClass().getAnnotation(Cacheable.class); + if (cacheable == null) { + /** Der Handler ist nicht mit @Cacheable annotiert: keine Dekorationen anbringen! */ return true; - - Cacheable cacheable = (Cacheable) handler; - - long now = System.currentTimeMillis(); - - /** - * Alle Antworten (insbesondere auch 304) sollen nach dem {@plainlink - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18 RFC 2616, - * Abschnitt 14.18} einen Date-Header enthalten - */ - response.setDateHeader(HEADER_DATE, now); - - /** Prüfen, ob der Handler willig ist, den Request zu verarbeiten */ - if (!cacheable.accepts(request)) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return false; } - /** Nichts weiter unternehmen, wenn der Handler dies nicht will */ - if (!cacheable.isGenerateCacheHeaders(request)) - return true; + /** CacheControll initialisieren (Handler nach annotierte Methoden scannen etc.) */ + cacheControl.init(handler); - String url = null; - if (log.isDebugEnabled()) { - if (request.getQueryString() == null) { - url = request.getRequestURI(); - } - else { - StringBuilder builder = new StringBuilder(); - builder.append(request.getRequestURI()); - builder.append('?'); - builder.append(request.getQueryString()); - url = builder.toString(); - } + if (cacheable.eager()) { + return cacheControl.decorate(request, response, handler); } - - int cacheSeconds = cacheable.getCacheSeconds(request); - if (cacheSeconds < 0) { - log.debug("{}: caching disabled!", url); - response.setDateHeader(HEADER_DATE, now); - response.setDateHeader(HEADER_EXPIRES, 0); - response.addHeader(HEADER_PRAGMA, "no-cache"); - response.addHeader(HEADER_CACHE_CONTROL, "private"); - response.addHeader(HEADER_CACHE_CONTROL, "no-cache"); - response.addHeader(HEADER_CACHE_CONTROL, "no-store"); - response.addHeader(HEADER_CACHE_CONTROL, "max-age=0"); - response.addHeader(HEADER_CACHE_CONTROL, "s-max-age=0"); + else { return true; } - - long ifModifiedSince = -1; - try { - ifModifiedSince = request.getDateHeader(HEADER_IF_MODIFIED_SINCE); - } - catch (Exception e) { - log.error("Exception while fetching If-Modified-Since: {}", e); - } - - long lastModified = cacheable.getLastModified(request); - - /** - * Sicherstellen, dass der Wert keine Millisekunden enthält, da die - * Zeitangabe aus dem Modified-Since-Header keine Millisekunden enthalten - * kann und der Test unten dann stets fehlschlagen würde! - */ - lastModified = lastModified - (lastModified % 1000); - - String ifNoneMatch = request.getHeader(HEADER_IF_NONE_MATCH); - String eTag = cacheable.getETag(request); - - /** - * 304-Antworten sollen nach dem {@plainlink - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 RFC - * 2616, Abschnitt 10.3.5} einen ETag-Header enthalten, wenn auch die - * 200-Antwort einen enthalten hätte. - */ - if (eTag != null) - response.setHeader(HEADER_ETAG, eTag); - - - if (ifModifiedSince >= lastModified && lastModified > 0) { - /** - * request.getDateHeader liefert die Zeit als long, oder -1, wenn der - * Header nicht existiert. D.h., wenn "If-Modified-Since" nicht gesetzt - * ist, wird die komplette Seite ausgeliefert. - * Der zusätzliche Test, ob lastModified größer 0 ist, ist nötig, um - * Fehler auszuschließen, wenn die Implementierung von Cachable - * negative Werte für Last-Modified zurückliefert. - */ - if (log.isDebugEnabled()) - log.debug("{}: Not modified since {}", url, new Date(ifModifiedSince)); - - if (ifNoneMatch == null) { - /** Neue Anfrage oder HTTP/1.0 Client! */ - log.debug("{}: ETag nicht gesetzt -> 304", url); - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - return false; - } - } - - if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) { - log.debug("{}: ETag {} not changed -> 304 ", url, ifNoneMatch); - response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); - return false; - } - - - log.debug("{}: first up!", url); - - /** HTTP/1.1-Caching-Header richtig setzen!! */ - response.setDateHeader(HEADER_LAST_MODIFIED, lastModified); - - /** Cache-Control für HTTP/1.1-Clients generieren */ - Map cacheControl = new HashMap(cacheable.getCacheControl(request)); - - /** - * Wenn eins JSESSIONID in der URL enthalten ist, darf die Anfrage nur vom - * Browser gecached werden! - */ - if (request.isRequestedSessionIdFromURL()) - cacheControl.put("private", null); - - if (cacheControl.containsKey("private")) { - /** - * HTTP/1.0 Caches davon abhalten, die Ressource zu cachen (vgl.: RFC - * 2616, {@plainlink - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3 - * Abschnitt 14.9.3} und {@plainlink - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.32 - * Abschnitt 14.32}) - */ - response.setDateHeader(HEADER_EXPIRES, 0l); - response.addHeader(HEADER_PRAGMA, "no-cache"); - } else { - /** - * Hier muss nicht geprüft werden, ob cacheSeconds > 0 gilt, da in diesem - * Fall oben bereits No-Cache-Header generiert und false - * zurückgeliefert werden! - * - * Den Wert als max-age zu den Schlüssel-Wert-Paaren für den - * Cache-Control-Header hinzufügen und einen entsprechenden - * Expires-Header für HTTP/1.0-Clients setzen. - */ - cacheControl.put("max-age", Integer.toString(cacheSeconds)); - response.setDateHeader(HEADER_EXPIRES, (now + (long) cacheSeconds * 1000)); - } - - StringBuilder builder = new StringBuilder(); - for (Entry entry : cacheControl.entrySet()) { - builder.setLength(0); - builder.append(entry.getKey()); - if (entry.getValue() != null) { - builder.append('='); - builder.append(entry.getValue()); - } - response.addHeader(HEADER_CACHE_CONTROL, builder.toString()); - } - - return true; } @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {} + public void postHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + ModelAndView modelAndView + ) throws Exception + { + cacheControl.decorate(request, response, handler); + } @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {} + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object handler, Exception ex + ) throws Exception + { + cacheControl.release(); + } + + + @Autowired + public void setCacheControl(CacheControl cacheControl) { + this.cacheControl = cacheControl; + } } diff --git a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/Cacheable.java b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/Cacheable.java deleted file mode 100644 index 75ac1102..00000000 --- a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/Cacheable.java +++ /dev/null @@ -1,153 +0,0 @@ -package de.halbekunst.juplo.cachecontrol; - - -import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - - -/** - * Wenn ein Handler (i.A. eine Impelementierung von {@Controller}), - * dieses Interface implementiert, dann schreibt - * {@link CachingInterceptor} HTTP/1.1-Caching-Header nach - * {@linkplain http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html RFC 2616} - * in den Response. - * - * @see CachingInterceptor - * @author kai - */ -public interface Cacheable { - public boolean accepts(HttpServletRequest request); - - /** - * Wenn die Methode false zurückliefert, werden von - * {@link CachingDispatcherServlet#isRessourceModified(HttpServletRequest, HttpServletResponse, Cacheable)} - * keinerlei HTTP/1.1-Cache-Header in den Response eingebaut. - *

- * Über diese Methode kann z.B. gesteuert werden, dass für eine bestimmte - * HTTP-Methode (z.B. POST) keine Cache-Header generiert werden. - * - * @see CachingDispatcherServlet - * @param request - * Der aktuelle HTTP-Request - * @return true, wenn Caching-Header in den Response geschrieben - * werden sollen, sonst false. - * @throws IllegalArgumentException - * Wenn zu dem Request keine Ressource existiert. - */ - public boolean isGenerateCacheHeaders(HttpServletRequest request) throws IllegalArgumentException; - - /** - * Diese Methode ermöglicht eine einfache, zentrale Steuerung des - * Caching-Verhaltens. - *

    - *
  • Wenn die Methode den Wert 0 (oder einen anderen Wert - * kleiner 1) zurückliefert, werden Cache-Header erzeugt, die das - * Cachen der Antwort für HTTP/1.0 und HTTP/1.1 vollständig untersagen,
  • - *
  • Wenn die Methode einen Wert größer 0 zurückliefert, wird - * ein für HTTP/1.0-Clients ein Expires-Header generiert und für - * HTTP/1.1-Clients ein Cache-Control-Header mit einem - * entsprechenden max-age-Eintrag. Dies reicht in Kombination mit - * einem sinnvollen Rückgabewert der Methode - * {@link #getLastModified(javax.servlet.http.HttpServletRequest)} vollständig - * für ein einfaches Caching aus.
  • - *
- *

- * Zu beachten: Wenn die Methode - * {@link #getCacheControl(javax.servlet.http.HttpServletRequest)} weitere - * Schlüssel-Wert-Paare für den Cache-Control-Header liefert, - * werden diese ergänzt. Wenn in der Rückgabe ein Wert für - * max-age enthalten ist, wir er allerdings von dem durch diese - * Methode vorgegebenen Wert überschrieben! - * - * @see #getLastModified(javax.servlet.http.HttpServletRequest) - * @see #getCacheControl(javax.servlet.http.HttpServletRequest) - * @param request - * Der aktuelle HTTP-Request - * @return Die gewünschte Cache-Zeit in Sekunden, oder 0, wenn - * Caching aktiv unterbunden werden soll bzw. einen Wert kleiner - * 0, wenn kein Expires-Header generiert - * werden soll. - * @throws IllegalArgumentException - * Wenn zu dem Request keine Ressource existiert. - */ - public int getCacheSeconds(HttpServletRequest request) throws IllegalArgumentException; - - /** - * Zeitpunkt, zu dem die Ressource zuletzt verändert wurde. Erwartet wird eine - * Zeitangabe in Millisekunden seit dem Unix-0-Zeitpunkt, wie sie von - * {@link HttpServletResponse#setDateHeader(String, long)} erwartet wird. - *

- * Zu beachten: - *

    - *
  • Diese Methode wird nicht aufgerufen, wenn - * {@link #getCacheSeconds(javax.servlet.http.HttpServletRequest)} - * 0 liefert.
  • - *
- * - * @see #getCacheSeconds(javax.servlet.http.HttpServletRequest) - * @param request - * Der aktuelle HTTP-Request - * @return Zeitstempel, zu dem die Ressource zuletzt modifiziert wurde. - * @throws IllegalArgumentException - * Wenn zu dem Request keine Ressource existiert. - */ - public long getLastModified(HttpServletRequest request) throws IllegalArgumentException; - - /** - * Frei wählbares ETag nach {@linkplain http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19 RFC 2616, - * Abschnitt 14.19 ETag}. Der ETag wird unverändert übernommen und muss den - * Bedingungen aus dem RFC 2616 entsprechen. Beispiele für erlaubte Werte: - *
    - *
  • "24afh2w3848adf" - *
  • W/"839482" - *
  • "" - *
- * Merke: Der wert ist immer in doppelte Anführungszeichen - * einzuschließen! - *

- * Zu beachten: - *

    - *
  • Diese Methode wird nicht aufgerufen, wenn - * {@link #getCacheSeconds(javax.servlet.http.HttpServletRequest)} - * 0 liefert.
  • - *
- * - * @see #getCacheSeconds(javax.servlet.http.HttpServletRequest) - * @param request - * Der aktuelle HTTP-Request - * @return Das zu verwendende ETag, oder null, wenn der Header - * nicht generiert werden soll. - * @throws IllegalArgumentException - * Wenn zu dem Request keine Ressource existiert. - */ - public String getETag(HttpServletRequest request) throws IllegalArgumentException; - - /** - * Diese Methode liefert eine Map mit Schlüssel-Wert-Paaren für den - * HTTP/1.1-Header Cache-Control (s. {@plainlink - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3 RFC2616, - * Abschnitt 14.9.3}). - *

- * Zu beachten: - *

    - *
  • Die Methode darf nie null zurückliefern!
  • - *
  • Ein Wert für den Schlüssel max-age wird überschrieben, - * wenn die Methode - * {@link #getCacheSeconds(javax.servlet.http.HttpServletRequest)} einen Wert - * größer 0 zurückliefert.
  • - *
  • Diese Methode wird nicht aufgerufen, wenn - * {@link #getCacheSeconds(javax.servlet.http.HttpServletRequest)} - * 0 liefert.
  • - *
- * - * @see #getCacheSeconds(javax.servlet.http.HttpServletRequest) - * @param request - * Der aktuelle HTTP-Request - * @return Eine Map mit den Schlüssel-Wert-Paaren für den - * Cache-Control-Header. - * @throws IllegalArgumentException - * Wenn zu dem Request keine Ressource existiert. - */ - public Map getCacheControl(HttpServletRequest request) throws IllegalArgumentException; -} diff --git a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/Accepts.java b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/Accepts.java new file mode 100644 index 00000000..2f6ec30e --- /dev/null +++ b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/Accepts.java @@ -0,0 +1,30 @@ +package de.halbekunst.juplo.cachecontrol.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mit dieser Methode kann eine Methode annotiert werden, die Auskunft darüber + * erteilt, mit welchem HTTP-Status-Code der Handler die Anfrage beatnworten + * wird. + *

+ * Die Methode muss eine Instanz von {@link HttpServletRequest} als (einziges!) + * Argument akzeptieren und einen Wert liefern, der sich nach + * int casten lässt. + *

+ * Eine mit dieser Annotation markierte Methode wird nur benötigt, wenn die + * Caching-Dekoration im Modus eager=true ausgeführt wird. Sie + * wird in diesem Fall benötigt, weil die Entscheidungen zur Cache-Dekoration + * dann getroffen werden müssen, bevor die verarbeitende Klasse die + * Anfrage verarbeitet hat. + * Wenn die Cache-Dekoration im Modus eager=true betrieben wird + * und keine Methode mit dieser Annotation annotiert ist, geht {@link CacheControl} + * davn aus, dass die verarbeitende Klasse alle Anfragen annimmt. + * + * @author kai + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Accepts {} diff --git a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/CacheControl.java b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/CacheControl.java new file mode 100644 index 00000000..ab5c4a36 --- /dev/null +++ b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/CacheControl.java @@ -0,0 +1,30 @@ +package de.halbekunst.juplo.cachecontrol.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Mit dieser Annotation kann eine Methode markiert werden, die die von Juplo- + * CacheControl für den Header Cache-Control generierten + * Schlüssel/Wert-Kombinationen manipulieren oder ergänzen kann, bevor der + * Header an den Client ausgeliefert wird (s. {@plainlink + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3 RFC2616, Abschnitt 14.9.3}). + *

+ * Die Methode muss zwei Parameter akzeptieren. + * Als ersten Parameter eine Instanz von {@link HttpServletRequest}. + * Als zweiten Parameter eine Map, die die von + * Juplo-CacheControl erzeugten Schlüssel/Wert-Paare enthält. + *

+ * Diese Methode liefert eine Map mit Schlüssel-Wert-Paaren für den + * HTTP/1.1-Header Cache-Control (s. {@plainlink + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3 RFC2616, + * Abschnitt 14.9.3}). + * + * @author kai + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface CacheControl { +} diff --git a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/CacheSeconds.java b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/CacheSeconds.java new file mode 100644 index 00000000..e076e087 --- /dev/null +++ b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/CacheSeconds.java @@ -0,0 +1,52 @@ +package de.halbekunst.juplo.cachecontrol.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.servlet.http.HttpServletRequest; + +/** + * Mit dieser Annotation können Klassen oder Methoden merkiert werden. + *

+ * Wenn eine Methode markiert wird, muss diese eine Instanz von + * {@link HttpServletRequest} als (einziges!) Argument akzeptieren und einen + * Wert liefern, der sich nach int casten lässt. + * Die annotierte Methode ermöglicht eine einfache, zentrale aber Request- + * Abhängige Steuerung des Caching-Verhaltens. + *

+ * Wenn eine Klasse annotiert wird, muss der Anotation die dann statisch für + * alle von der Klasse erzeugten Antworten gültige Cache-Zeit als Argument + * übergeben werden. + * Wird keine Cache-Zeit spezifiziert, wird der Wert 86400 + * (ein Tag) verwendet. + *

    + *
  • Wenn negativer Wert als Cache-Seconds festgelet wird, werden Cache-Header + * erzeugt, die das Cachen der Antwort für HTTP/1.0 und HTTP/1.1 vollständig + * untersagen.
  • + *
  • Wenn einen Wert größer oder gleich 0 festgelegt wird, wird + * für HTTP/1.0-Clients ein Expires-Header generiert und für + * HTTP/1.1-Clients ein Cache-Control-Header mit einem + * entsprechenden max-age-Eintrag. Dies reicht in Kombination mit + * der Annotation {@link LastModified} vollständig für ein einfaches aber + * effektives Caching aus.
  • + *
+ *

+ * TODO + * Zu beachten: Wenn die Methode + * {@link #getCacheControl(javax.servlet.http.HttpServletRequest)} weitere + * Schlüssel-Wert-Paare für den Cache-Control-Header liefert, + * werden diese ergänzt. Wenn in der Rückgabe ein Wert für + * max-age enthalten ist, wir er allerdings von dem durch diese + * Methode vorgegebenen Wert überschrieben! + * + * @author kai + * @See Cacheable + * @See LastModified + * @See CacheControl + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface CacheSeconds { + int value() default 86400; /** Default: 1 Tag */ +} diff --git a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/Cacheable.java b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/Cacheable.java new file mode 100644 index 00000000..7a1be267 --- /dev/null +++ b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/Cacheable.java @@ -0,0 +1,45 @@ +package de.halbekunst.juplo.cachecontrol.annotations; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Marker-Annotation für Handler (i.A. eine Impelementierung von + * {@link Controller}), deren Antworten vom {@link CachingInterceptor} mit + * HTTP/1.1-Caching-Header nach + * {@linkplain http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html RFC 2616} + * dekoriert werden sollen. + *

+ * Wenn der Parameter eager auf true gesetzt wird, + * ermittelt der {@link CachingInterceptor} die Cache-Parameter über die + * annotierten Methoden vorab. + * Achtung: + * Dies bedeutet, dass die annotierten Methoden aufgerufen werden bevor + * die eigentliche Verarbeitungs-Routine der markierten Klasse aufgerufen wird! + * Wenn sich dabei ergiebt, dass die Antwort nicht erneut ausgeliefert werden + * muss, wird die eigentliche Verarbeitungs-Routine gar nicht aufgerufen. + *

+ * Wenn der Parameter eager nicht gesetzt ist (oder explizit auf + * false gesetzt wurde), kapselt der {@link CachingInterceptor} + * den Request und den Ausgabestrom für den Response-Body und trifft die + * Entscheidung über die zu ergänzenden Header, wenn der Status des + * {@link HttpServletResponse} gesetzt oder mit dem Schreiben des Response-Body + * begonnen wird. + * + * @see CacheControl + * @see Accepts + * @see CacheSeconds + * @see LastModified + * @see ETag + * @see CacheControl + * @author kai + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Cacheable { + boolean eager() default false; +} diff --git a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/ETag.java b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/ETag.java new file mode 100644 index 00000000..d2542dc8 --- /dev/null +++ b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/ETag.java @@ -0,0 +1,84 @@ +package de.halbekunst.juplo.cachecontrol.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.servlet.http.HttpServletRequest; + +/** + * Über diese Annotation kann der Inhalt des ETag/code>-Headers + * gesteuert werden. + * Mit dieser Annotation können Klassen oder Methoden merkiert werden. + *

+ * Wenn eine Methode annotiert wird, muss diese eine Instanz von + * {@link HttpServletRequest} als (einziges!) Argument akzeptieren und einen + * String liefern. + *

+ * Wenn eine Klasse Annotiert wird, muss der Annotation der Wert für den + * ETag-Header übergeben werden. + * Da dieser Wert somit statisch ist, macht es nur Sinn, Klassen mit dieser + * Annotation zu markieren, die ausschließlich statische Ressourcen ausliefern, + * die sich nur mit der Neuinstallation der Webanwendung ändern. + * Wenn sich (z.B. nach einer Neuinstallation der Webanwendung) die statischen + * Ressourcen geändert haben, muss der übergebene statische ETag geändert + * werden, da Caches sonst weiterhin die alten Ressourcen ausliefern! + *

+ * Frei wählbares ETag nach + * {@linkplain http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19 RFC 2616, Abschnitt 14.19 ETag}. + * Der gelieferte Wert darf die vom RFC geforderten Anführungszeichen noch nicht + * enthalten, da er, wenn vary gesetzt ist, noch um je nach + * erfolgter Content-Negotiation varriierende Teile ergänzt wird. + *

+ * Die erzeugten ETag's können über die Annotations-Parameter + * weak und vary weiter gesteuert werden. + *

    + *
  • + * Wenn der Parameter weak auf den wert true + * gesetzt wird, wird ein schwaches ETag erezeugt und der + * Vergleichs-Algorithmus verhält sich entsprechend anders (siehe: + * {@linkplain http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3 RFC 2616, Abschnitt 13.3.3 Weak and Strong Validators}). + *
  • + *
  • + * Über den Parameter vary kann Juplo-CacheControl damit + * beauftragt werden, die Nötigen Maßnahmen für korrektes Content-Negotiating + * zu ergreifen (siehe: + * {@linkplain http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.6 RFC 2616, Abschnitt 13.6 Caching Negotiated Responses}). + * Als Eingabe werden die Header-Namen erwertet, die zu unterschiedlichen + * Ergebnissen der Content-Negotiation führen können (hier können folgende + * Header angegeben werden: Accept, Accept-Charset, + * Accept-Encoding und Accept-Language). + * Juplo-CacheControl modifizert den übergebenen ETag dann so, + * dass unterschiedliche Resultate der Content-Negotiation unterschieden + * werden können. + * Außerdem wird der Vary-Header entsprechend gesetzt. + *
  • + *
+ * Zu beachten: + * Wenn zugleich die Annotation {@link CacheSeconds} verwendet wird, wird + * die mit dieser Annotation markierte Methode nur aufgerufen, wenn die mit + * der Annotation {@link CacheSeconds} markierte Methode einen Wert größer + * oder gleich 0 liefert, bzw. für die mit Annotation + * {@link CacheSeconds} markierte Klasse eine Cache-Zeit größer oder gleich + * 0 festgelegt wurde. + * + * @see #getCacheSeconds(javax.servlet.http.HttpServletRequest) + * + * @author kai + * @see Cacheable + * @see CacheSeconds + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface ETag { + + public final static String ACCEPT = "Accept"; + public final static String ACCEPT_CHARSET = "Accept-Charset"; + public final static String ACCEPT_ENCODING = "Accept-Encoding"; + public final static String ACCEPT_LANGUAGE = "Accept-Language"; + + + String value() default "X"; + boolean weak() default false; + String[] vary() default {}; +} diff --git a/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/LastModified.java b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/LastModified.java new file mode 100644 index 00000000..be28e0ea --- /dev/null +++ b/cachecontrol/src/main/java/de/halbekunst/juplo/cachecontrol/annotations/LastModified.java @@ -0,0 +1,53 @@ +package de.halbekunst.juplo.cachecontrol.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; + +/** + * Über diese Annotation kann der Inhalt des Last-Modified-Headers + * gesteuert werden. + * Mit dieser Annotation können Klassen oder Methoden merkiert werden. + *

+ * Wenn eine Methode annotiert wird, muss diese eine Instanz von + * {@link HttpServletRequest} als (einziges!) Argument akzeptieren und einen + * Wert liefern, der sich nach long casten lässt. + * Die Signatur der Methode entspricht der Methode + * {@link HttpServlet#getLastModified(javax.servlet.http.HttpServletRequest)} + * aus dem HttpServlet-Interface. + * Um das Cache-Verhalten ein existierendes Servlet, das diese Methode bereits + * implementiert, mit Juplo-CacheControll zu verbessern, kann als erste + * Maßnahme daher einfach diese Methode mit dieser Annotation markiert werden. + *

+ * Wenn eine Klasse Annotiert wird, muss der Annotation der Wert für den + * Last-Modified-Header übergeben werden. + * Da dieser Wert somit statisch ist, macht es nur Sinn, Klassen mit dieser + * Annotation zu markieren, die ausschließlich statische Ressourcen ausliefern, + * die sich nur mit der Neuinstallation der Webanwendung ändern. + *

+ * Über diese Annotation wird der Zeitpunkt gesteuert, zu dem die gelieferte + * Ressource zuletzt verändert wurde. + * Erwartet wird eine Zeitangabe in Millisekunden seit dem Unix-0-Zeitpunkt, + * die dann an {@link HttpServletResponse#setDateHeader(String, long)} + * weitergegeben wird. + *

+ * Zu beachten: + * Wenn zugleich die Annotation {@link CacheSeconds} verwendet wird, wird + * die mit dieser Annotation markierte Methode nur aufgerufen, wenn die mit + * der Annotation {@link CacheSeconds} markierte Methode einen Wert größer + * oder gleich 0 liefert, bzw. für die mit Annotation + * {@link CacheSeconds} markierte Klasse eine Cache-Zeit größer oder gleich + * 0 festgelegt wurde. + * + * @author kai + * @see Cacheable + * @see CacheSeconds + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +public @interface LastModified { + long value() default 0; +} diff --git a/pom.xml b/pom.xml index abd2bf79..2db8fc08 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ de.halbekunst juplo - 1.0.1 + 2.0-SNAPSHOT Juplo pom http://www.halbekunst.de @@ -24,9 +24,7 @@ - test cachecontrol - utils