/* Copyright 2007 Ben Gunter * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.sourceforge.stripes.controller; import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.http.HttpServletResponse; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.HttpCache; import net.sourceforge.stripes.action.Resolution; import net.sourceforge.stripes.util.Log; /** * Looks for an {@link HttpCache} annotation on the event handler method, the {@link ActionBean} * class or the {@link ActionBean}'s superclasses. If an {@link HttpCache} is found, then the * appropriate response headers are set to control client-side caching. * * @author Ben Gunter * @since Stripes 1.5 */ @Intercepts(LifecycleStage.ResolutionExecution) public class HttpCacheInterceptor implements Interceptor { private static final Log logger = Log.getInstance(HttpCacheInterceptor.class); @HttpCache private static final class CacheKey { final Method method; final Class<?> beanClass; final int hashCode; /** Create a cache key for the given event handler method and {@link ActionBean} class. */ public CacheKey(Method method, Class<? extends ActionBean> beanClass) { this.method = method; this.beanClass = beanClass; this.hashCode = method.hashCode() * 37 + beanClass.hashCode(); } @Override public boolean equals(Object obj) { final CacheKey that = (CacheKey) obj; return this.method.equals(that.method) && this.beanClass.equals(that.beanClass); } @Override public int hashCode() { return hashCode; } @Override public String toString() { return beanClass.getName() + "." + method.getName() + "()"; } } private Map<CacheKey, HttpCache> cache = new ConcurrentHashMap<CacheKey, HttpCache>(128); /** Null values are not allowed by {@link ConcurrentHashMap} so use this reference instead. */ private static final HttpCache NULL_CACHE = CacheKey.class.getAnnotation(HttpCache.class); public Resolution intercept(ExecutionContext ctx) throws Exception { final ActionBean actionBean = ctx.getActionBean(); final Method handler = ctx.getHandler(); if (actionBean != null && handler != null) { final Class<? extends ActionBean> beanClass = actionBean.getClass(); // if caching is disabled, then set the appropriate response headers logger.debug("Looking for ", HttpCache.class.getSimpleName(), " on ", beanClass.getName(), ".", handler.getName(), "()"); final HttpCache annotation = getAnnotation(handler, beanClass); if (annotation != null) { final HttpServletResponse response = ctx.getActionBeanContext().getResponse(); if (annotation.allow()) { long expires = annotation.expires(); if (expires != HttpCache.DEFAULT_EXPIRES) { logger.debug("Response expires in ", expires, " seconds"); expires = expires * 1000 + System.currentTimeMillis(); response.setDateHeader("Expires", expires); } } else { logger.debug("Disabling client-side caching for response"); response.setDateHeader("Expires", 0); response.setHeader("Cache-control", "no-store, no-cache, must-revalidate"); response.setHeader("Pragma", "no-cache"); } } } return ctx.proceed(); } /** * Look for a {@link HttpCache} annotation on the method first and then on the class and its * superclasses. * * @param method an event handler method * @param beanClass the class to inspect for annotations if none is found on the method * @return The first {@link HttpCache} annotation found. If none is found then null. */ protected HttpCache getAnnotation(Method method, Class<? extends ActionBean> beanClass) { // check cache first final CacheKey cacheKey = new CacheKey(method, beanClass); HttpCache annotation = cache.get(cacheKey); if (annotation != null) { return annotation == NULL_CACHE ? null : annotation; } // not found in cache so figure it out annotation = method.getAnnotation(HttpCache.class); if (annotation == null) { // search the method's class and its superclasses Class<?> clazz = beanClass; do { annotation = clazz.getAnnotation(HttpCache.class); clazz = clazz.getSuperclass(); } while (clazz != null && annotation == null); } // check for weirdness if (annotation != null) { logger.debug("Found ", HttpCache.class.getSimpleName(), " for ", beanClass.getName(), ".", method.getName(), "()"); final int expires = annotation.expires(); if (annotation.allow() && expires != HttpCache.DEFAULT_EXPIRES && expires < 0) { logger.warn(HttpCache.class.getSimpleName(), " for ", beanClass.getName(), ".", method.getName(), "() allows caching but expires in the past"); } else if (!annotation.allow() && expires != HttpCache.DEFAULT_EXPIRES) { logger.warn(HttpCache.class.getSimpleName(), " for ", beanClass.getName(), ".", method.getName(), "() disables caching but explicitly sets expires"); } } else { annotation = NULL_CACHE; } cache.put(cacheKey, annotation); return annotation; } }