Revision: 646
          http://stripes.svn.sourceforge.net/stripes/?rev=646&view=rev
Author:   bengunter
Date:     2007-12-05 21:13:14 -0800 (Wed, 05 Dec 2007)

Log Message:
-----------
STS-449: Add annotation to control client-side caching. The new @HttpCache 
annotation allows one to turn caching on or off and to expire a document after 
a number of seconds.

Modified Paths:
--------------
    trunk/stripes/src/net/sourceforge/stripes/config/DefaultConfiguration.java

Added Paths:
-----------
    trunk/stripes/src/net/sourceforge/stripes/action/HttpCache.java
    
trunk/stripes/src/net/sourceforge/stripes/controller/HttpCacheInterceptor.java

Added: trunk/stripes/src/net/sourceforge/stripes/action/HttpCache.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/action/HttpCache.java             
                (rev 0)
+++ trunk/stripes/src/net/sourceforge/stripes/action/HttpCache.java     
2007-12-06 05:13:14 UTC (rev 646)
@@ -0,0 +1,62 @@
+/* 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.action;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * <p>
+ * This annotation can be applied to an event handler method or to an [EMAIL 
PROTECTED] ActionBean} class to
+ * suggest to the HTTP client how it should cache the response. Classes will 
inherit this annotation
+ * from their superclass. Method-level annotations override class-level 
annotations. This means, for
+ * example, that applying [EMAIL PROTECTED] @HttpCache(allow=false)} to an 
[EMAIL PROTECTED] ActionBean} class turns off
+ * client-side caching for all events except those that are annotated with
+ * [EMAIL PROTECTED] @HttpCache(allow=true)}.
+ * </p>
+ * <p>
+ * Some examples:
+ * <ul>
+ * <li>[EMAIL PROTECTED] @HttpCache} - Same behavior as if the annotation were 
not present. No headers are
+ * set.</li>
+ * <li>[EMAIL PROTECTED] @HttpCache(allow=true)} - Same as above.</li>
+ * <li>[EMAIL PROTECTED] @HttpCache(allow=false)} - Set headers to disable 
caching and immediately expire the
+ * document.</li>
+ * <li>[EMAIL PROTECTED] @HttpCache(expires=3600)} - Caching is allowed. The 
document expires in 10 minutes.</li>
+ * </ul>
+ * </p>
+ * 
+ * @author Ben Gunter
+ * @since Stripes 1.5
+ */
[EMAIL PROTECTED](RetentionPolicy.RUNTIME)
[EMAIL PROTECTED]( { ElementType.METHOD, ElementType.TYPE })
[EMAIL PROTECTED]
[EMAIL PROTECTED]
+public @interface HttpCache {
+    /** Indicates whether the response should be cached by the client. */
+    boolean allow() default true;
+
+    /**
+     * The number of seconds into the future that the response should expire. 
If [EMAIL PROTECTED] #allow()} is
+     * false, then this value is ignored and zero is used. If [EMAIL 
PROTECTED] #allow()} is true and this
+     * value is less than zero, then no Expires header is sent.
+     */
+    int expires() default 0;
+}

Modified: 
trunk/stripes/src/net/sourceforge/stripes/config/DefaultConfiguration.java
===================================================================
--- trunk/stripes/src/net/sourceforge/stripes/config/DefaultConfiguration.java  
2007-12-04 17:18:08 UTC (rev 645)
+++ trunk/stripes/src/net/sourceforge/stripes/config/DefaultConfiguration.java  
2007-12-06 05:13:14 UTC (rev 646)
@@ -28,6 +28,7 @@
 import net.sourceforge.stripes.controller.BeforeAfterMethodInterceptor;
 import net.sourceforge.stripes.controller.DefaultActionBeanContextFactory;
 import net.sourceforge.stripes.controller.DefaultActionBeanPropertyBinder;
+import net.sourceforge.stripes.controller.HttpCacheInterceptor;
 import net.sourceforge.stripes.controller.Interceptor;
 import net.sourceforge.stripes.controller.Intercepts;
 import net.sourceforge.stripes.controller.LifecycleStage;
@@ -398,6 +399,7 @@
     protected Map<LifecycleStage, Collection<Interceptor>> 
initCoreInterceptors() {
         Map<LifecycleStage, Collection<Interceptor>> interceptors = new 
HashMap<LifecycleStage, Collection<Interceptor>>();
         addInterceptor(interceptors, new BeforeAfterMethodInterceptor());
+        addInterceptor(interceptors, new HttpCacheInterceptor());
         return interceptors;
     }
 

Added: 
trunk/stripes/src/net/sourceforge/stripes/controller/HttpCacheInterceptor.java
===================================================================
--- 
trunk/stripes/src/net/sourceforge/stripes/controller/HttpCacheInterceptor.java  
                            (rev 0)
+++ 
trunk/stripes/src/net/sourceforge/stripes/controller/HttpCacheInterceptor.java  
    2007-12-06 05:13:14 UTC (rev 646)
@@ -0,0 +1,169 @@
+/* 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.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import net.sourceforge.stripes.action.ActionBean;
+import net.sourceforge.stripes.action.ActionBeanContext;
+import net.sourceforge.stripes.action.HttpCache;
+import net.sourceforge.stripes.action.Resolution;
+import net.sourceforge.stripes.config.Configuration;
+import net.sourceforge.stripes.util.Log;
+
+/**
+ * Looks for an [EMAIL PROTECTED] HttpCache} annotation on the event handler 
method, the [EMAIL PROTECTED] ActionBean}
+ * class or the [EMAIL PROTECTED] ActionBean}'s superclasses. If an [EMAIL 
PROTECTED] HttpCache} is found, then the
+ * appropriate response headers are set to control client-side caching.
+ * 
+ * @author Ben Gunter
+ * @since Stripes 1.5
+ */
[EMAIL PROTECTED](LifecycleStage.ResolutionExecution)
+public class HttpCacheInterceptor implements Interceptor {
+    private static final class CacheKey {
+        private Method method;
+        private Class<?> beanClass;
+        private int hashCode;
+
+        /** Create a cache key for the given event handler method and [EMAIL 
PROTECTED] 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) {
+            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 static final Log logger = 
Log.getInstance(HttpCacheInterceptor.class);
+
+    private Map<CacheKey, HttpCache> cache = new HashMap<CacheKey, 
HttpCache>(128);
+
+    public Resolution intercept(ExecutionContext ctx) throws Exception {
+        final Configuration config = StripesFilter.getConfiguration();
+        final ActionResolver resolver = config.getActionResolver();
+        final ActionBeanContext context = ctx.getActionBeanContext();
+        final ActionBean actionBean = resolver.getActionBean(context);
+        final Class<? extends ActionBean> beanClass = actionBean.getClass();
+        final String eventName = resolver.getEventName(beanClass, context);
+
+        // Look up the event handler method
+        final Method handler;
+        if (eventName != null) {
+            handler = resolver.getHandler(beanClass, eventName);
+        }
+        else {
+            handler = resolver.getDefaultHandler(beanClass);
+            if (handler != null) {
+                context.setEventName(resolver.getHandledEvent(handler));
+            }
+        }
+
+        if (handler != null) {
+            // if caching is disabled, then set the appropriate response 
headers
+            logger.debug("Looking for ", HttpCache.class.getSimpleName(), " on 
", beanClass
+                    .getName(), ".", handler.getName(), "()");
+            HttpCache annotation = getAnnotation(handler, beanClass);
+            if (annotation != null) {
+                HttpServletResponse response = context.getResponse();
+                if (annotation.allow()) {
+                    long expires = annotation.expires();
+                    if (expires >= 0) {
+                        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-cache");
+                    response.setHeader("Pragma", "no-cache");
+                }
+            }
+        }
+        else {
+            logger.warn("No handler method found for ActionBean [", 
beanClass.getName(),
+                    "] and eventName [ ", eventName, "]");
+        }
+
+        return ctx.proceed();
+    }
+
+    /**
+     * Look for a [EMAIL PROTECTED] 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 [EMAIL PROTECTED] HttpCache} annotation found. If 
none is found then null.
+     */
+    protected HttpCache getAnnotation(Method method, Class<? extends 
ActionBean> beanClass) {
+        // check cache first
+        CacheKey cacheKey = new CacheKey(method, beanClass);
+        if (cache.containsKey(cacheKey)) {
+            HttpCache annotation = cache.get(cacheKey);
+            return annotation;
+        }
+
+        // not found in cache so figure it out
+        HttpCache 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(), "()");
+            if (annotation.allow() && annotation.expires() < 0) {
+                logger.warn(HttpCache.class.getSimpleName(), " for ", 
beanClass.getName(), ".",
+                        method.getName(), "() allows caching but expires in 
the past");
+            }
+            else if (!annotation.allow() && annotation.expires() != 0) {
+                logger.warn(HttpCache.class.getSimpleName(), " for ", 
beanClass.getName(), ".",
+                        method.getName(), "() disables caching but explicitly 
sets expires");
+            }
+        }
+
+        // cache and return it
+        cache.put(cacheKey, annotation);
+        return annotation;
+    }
+}


This was sent by the SourceForge.net collaborative development platform, the 
world's largest Open Source development site.

-------------------------------------------------------------------------
SF.Net email is sponsored by: The Future of Linux Business White Paper
from Novell.  From the desktop to the data center, Linux is going
mainstream.  Let it simplify your IT future.
http://altfarm.mediaplex.com/ad/ck/8857-50307-18918-4
_______________________________________________
Stripes-development mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/stripes-development

Reply via email to