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;
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<CacheMethodHandle> tl = new ThreadLocal<CacheMethodHandle>();
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 void init(CacheMethodHandle handle) {
+ CacheControl.tl.set(handle);
+ }
+
+ public void init(Object handler) throws NoSuchMethodException {
+ CacheControl.tl.set(new ReflectionCacheMethodHandle(handler));
}
public boolean decorate(
HttpServletRequest request,
HttpServletResponse response,
Object handler
- ) throws Exception
+ )
{
try {
CacheMethodHandle controller = CacheControl.tl.get();
* 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());
+ response.setDateHeader(HeaderNames.HEADER_DATE, controller.getTimestamp());
/** Besondere Maßnahmen für besondere HTTP-Status-Codes ?!? */
int status = controller.accepts(request);
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:
/**
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");
+ response.setDateHeader(HeaderNames.HEADER_DATE, controller.getTimestamp());
+ response.setDateHeader(HeaderNames.HEADER_EXPIRES, 0);
+ response.addHeader(HeaderNames.HEADER_PRAGMA, "no-cache");
+ response.addHeader(HeaderNames.HEADER_CACHE_CONTROL, "private");
+ response.addHeader(HeaderNames.HEADER_CACHE_CONTROL, "no-cache");
+ response.addHeader(HeaderNames.HEADER_CACHE_CONTROL, "no-store");
+ response.addHeader(HeaderNames.HEADER_CACHE_CONTROL, "max-age=0");
+ response.addHeader(HeaderNames.HEADER_CACHE_CONTROL, "s-max-age=0");
return true;
}
long ifModifiedSince = -1;
try {
- ifModifiedSince = request.getDateHeader(HEADER_IF_MODIFIED_SINCE);
+ ifModifiedSince = request.getDateHeader(HeaderNames.HEADER_IF_MODIFIED_SINCE);
}
catch (Exception e) {
log.error("Exception while fetching If-Modified-Since: {}", e);
*/
lastModified = lastModified - (lastModified % 1000);
- String ifNoneMatch = request.getHeader(HEADER_IF_NONE_MATCH);
+ String ifNoneMatch = request.getHeader(HeaderNames.HEADER_IF_NONE_MATCH);
String eTag = controller.getETag(request);
/**
* 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 (eTag != null) {
+ StringBuilder builder = new StringBuilder();
+ if (controller.isETagWeak())
+ builder.append("W/");
+ builder.append('"');
+ builder.append(eTag);
+ builder.append('"');
+ response.setHeader(HeaderNames.HEADER_ETAG, builder.toString());
+ }
if (ifModifiedSince >= lastModified && lastModified > 0) {
}
}
- if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) {
- log.debug("{}: ETag {} not changed -> 304 ", url, ifNoneMatch);
- response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
- return false;
+ if (ifNoneMatch != null) {
+ boolean weak = false;
+ if (ifNoneMatch.startsWith("W/")) {
+ weak = true;
+ ifNoneMatch = ifNoneMatch.substring(3, ifNoneMatch.length() - 1);
+ }
+ else {
+ ifNoneMatch = ifNoneMatch.substring(1, ifNoneMatch.length() - 1);
+ }
+
+ if (!weak || (request.getMethod().equals("GET") && request.getHeader(HeaderNames.HEADER_RANGE) == null)) {
+ /**
+ * Die Gleichheit gilt nur, wenn die ETag's der Anfrage _und_ der
+ * Antwort stark sind (starke Gleichheit!), oder wenn die Antwort nur
+ * schwache Gleichheit fordert...
+ */
+ if (ifNoneMatch.equals(eTag) && (controller.isETagWeak() || !weak)) {
+ log.debug("{}: ETag {} not changed -> 304 ", url, ifNoneMatch);
+ response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
+ return false;
+ }
+ }
+ else {
+ log.warn("{}: ignoring weak ETag W/\"{}\", because the request was no GET-request or the Range-Header was present!", url, ifNoneMatch);
+ }
}
log.debug("{}: first up!", url);
/** HTTP/1.1-Caching-Header richtig setzen!! */
- response.setDateHeader(HEADER_LAST_MODIFIED, lastModified);
+ response.setDateHeader(HeaderNames.HEADER_LAST_MODIFIED, lastModified);
/** Cache-Control für HTTP/1.1-Clients generieren */
Map<String, String> cacheControl = new TreeMap<String, String>();
* <code>Expires</code>-Header für HTTP/1.0-Clients setzen.
*/
cacheControl.put("max-age", Integer.toString(cacheSeconds));
- response.setDateHeader(HEADER_EXPIRES, (controller.getTimestamp() + (long) cacheSeconds * 1000));
+ response.setDateHeader(HeaderNames.HEADER_EXPIRES, (controller.getTimestamp() + (long) cacheSeconds * 1000));
}
/** Dem Handler die Gelegenheit geben, den Cache-Controll-Header anzupassen */
* 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");
+ response.setDateHeader(HeaderNames.HEADER_EXPIRES, 0l);
+ response.addHeader(HeaderNames.HEADER_PRAGMA, "no-cache");
}
StringBuilder builder = new StringBuilder();
builder.append('=');
builder.append(entry.getValue());
}
- response.addHeader(HEADER_CACHE_CONTROL, builder.toString());
+ response.addHeader(HeaderNames.HEADER_CACHE_CONTROL, builder.toString());
}
return true;
}
- interface CacheMethodHandle {
+ public 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<String, String> cacheControlMap) throws Exception;
+ int accepts(HttpServletRequest request);
+ int getCacheSeconds(HttpServletRequest request);
+ long getLastModified(HttpServletRequest request);
+ String getETag(HttpServletRequest request);
+ boolean isETagWeak();
+ void cacheControl(HttpServletRequest request, Map<String, String> cacheControlMap);
}
- 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<String, String> 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;
+ private long now = System.currentTimeMillis();
+ private Integer cacheSeconds;
+ private Long lastModified;
+ private String eTag;
+ private Method acceptsMethod;
+ private Method cacheSecondsMethod;
+ private Method lastModifiedMethod;
+ private Method eTagMethod;
+ private Method cacheControlMethod;
+ private boolean isAcceptsMethodDefined;
+ private boolean isCacheSecondsMethodDefined;
+ private boolean isLastModifiedMethodDefined;
+ private boolean isETagMethodDefined;
+ private boolean isCacheControlMethodDefined;
+ private boolean weak;
ReflectionCacheMethodHandle(Object handler) throws NoSuchMethodException {
+
this.handler = handler;
+
+ cacheSeconds = CacheControl.this.defaultCacheSeconds;
+ lastModified = CacheControl.this.defaultLastModified;
+ eTag = "";
+
/** Class-Level-Annotations auslesen */
for (Annotation annotation : handler.getClass().getAnnotations()) {
if (annotation.annotationType().equals(CacheSeconds.class)) {
- defaults.cacheSeconds = ((CacheSeconds)annotation).value();
- isCacheSecondsDefined = true;
+ cacheSeconds = ((CacheSeconds)annotation).value();
+ isCacheSecondsMethodDefined = true;
continue;
}
if (annotation.annotationType().equals(LastModified.class)) {
- defaults.lastModified = ((LastModified)annotation).value();
- if (defaults.lastModified < 1) {
+ lastModified = ((LastModified)annotation).value();
+ if (lastModified < 1) {
/**
* Ein Last-Modified-Header wurde angefordert, aber es wurde kein
* statischer Wert spezifiziert:
* globalen statischen Default-Wert benutzen!
*/
- defaults.lastModified = defaultLastModified;
+ lastModified = defaultLastModified;
}
- isLastModifiedDefined = true;
+ isLastModifiedMethodDefined = true;
continue;
}
if (annotation.annotationType().equals(ETag.class)) {
- defaults.eTag = ((ETag)annotation).value();
- isETagDefined = true;
+ ETag eTagAnnotation = (ETag)annotation;
+ eTag = eTagAnnotation.value();
+ weak = eTagAnnotation.weak();
+ isETagMethodDefined = true;
continue;
}
}
for (Method method : handler.getClass().getMethods()) {
for (Annotation annotation : method.getAnnotations()) {
if (annotation.annotationType().equals(Accepts.class)) {
- if (isAcceptsDefined)
+ if (isAcceptsMethodDefined)
throw new IllegalArgumentException("Die Annotation @Accept wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
- accepts = method;
- isAcceptsDefined = true;
+ acceptsMethod = method;
+ isAcceptsMethodDefined = true;
continue;
}
if (annotation.annotationType().equals(CacheSeconds.class)) {
- if (isCacheSecondsDefined)
+ if (isCacheSecondsMethodDefined)
throw new IllegalArgumentException("Die Annotation @CacheSeconds wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
- cacheSeconds = method;
- isCacheSecondsDefined = true;
+ cacheSecondsMethod = method;
+ isCacheSecondsMethodDefined = true;
continue;
}
if (annotation.annotationType().equals(LastModified.class)) {
- if (isLastModifiedDefined)
+ if (isLastModifiedMethodDefined)
throw new IllegalArgumentException("Die Annotation @LastModified wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
- lastModified = method;
- isLastModifiedDefined = true;
+ lastModifiedMethod = method;
+ isLastModifiedMethodDefined = true;
continue;
}
if (annotation.annotationType().equals(ETag.class)) {
- if (isETagDefined)
+ if (isETagMethodDefined)
throw new IllegalArgumentException("Die Annotation @ETag wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
- eTag = method;
- isETagDefined = true;
+ eTagMethod = method;
+ weak = ((ETag)annotation).weak();
+ isETagMethodDefined = true;
continue;
}
if (annotation.annotationType().equals(de.halbekunst.juplo.cachecontrol.annotations.CacheControl.class)) {
- if (isCacheControlDefined)
+ if (isCacheControlMethodDefined)
throw new IllegalArgumentException("Die Annotation @CacheControl wurde in der Klasse " + handler.getClass().getSimpleName() + " mehrfach verwendet!");
- cacheControl = method;
- isCacheControlDefined = true;
+ cacheControlMethod = method;
+ isCacheControlMethodDefined = true;
continue;
}
}
@Override
public long getTimestamp() {
- return defaults.now;
+ return 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);
+ public int accepts(HttpServletRequest request) throws IllegalArgumentException {
+ if (acceptsMethod == null) {
+ return HttpServletResponse.SC_OK;
+ }
+ else {
+ try {
+ return (Integer)acceptsMethod.invoke(handler, request);
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
}
@Override
- public int getCacheSeconds(HttpServletRequest request)
- throws IllegalAccessException,
- IllegalArgumentException,
- InvocationTargetException
- {
- if (cacheSeconds == null)
- return defaults.getCacheSeconds(request);
- else
- return (Integer)cacheSeconds.invoke(handler, request);
+ public int getCacheSeconds(HttpServletRequest request) throws IllegalArgumentException {
+ if (cacheSecondsMethod == null) {
+ return cacheSeconds;
+ }
+ else {
+ try {
+ return (Integer)cacheSecondsMethod.invoke(handler, request);
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
}
@Override
- public long getLastModified(HttpServletRequest request)
- throws IllegalAccessException,
- IllegalArgumentException,
- InvocationTargetException
- {
- if (lastModified == null)
- return defaults.getLastModified(request);
- else
- return (Long)lastModified.invoke(handler, request);
+ public long getLastModified(HttpServletRequest request) throws IllegalArgumentException {
+ if (lastModifiedMethod == null) {
+ return lastModified;
+ }
+ else {
+ try {
+ return (Long)lastModifiedMethod.invoke(handler, request);
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
}
@Override
- public String getETag(HttpServletRequest request)
- throws IllegalAccessException,
- IllegalArgumentException,
- InvocationTargetException
- {
- if (eTag == null)
- return defaults.getETag(request);
- else
- return (String)eTag.invoke(handler, request);
+ public String getETag(HttpServletRequest request) throws IllegalArgumentException {
+ if (eTagMethod == null) {
+ return eTag;
+ }
+ else {
+ try {
+ return (String)eTagMethod.invoke(handler, request);
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+ }
+
+ @Override
+ public boolean isETagWeak() {
+ return weak;
}
@Override
HttpServletRequest request,
Map<String, String> cacheControlMap
)
- throws IllegalAccessException,
- IllegalArgumentException,
- InvocationTargetException
+ throws IllegalArgumentException
{
- if (cacheControl == null)
- defaults.cacheControl(request, cacheControlMap);
- else
- cacheControl.invoke(handler, request, cacheControlMap);
+ if (cacheControlMethod != null) {
+ try {
+ cacheControlMethod.invoke(handler, request, cacheControlMap);
+ }
+ catch (Exception e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
}
}