Anonymous avatar Anonymous committed c51d171

CACHE-308 Support weak ETags header

Comments (0)

Files changed (5)

src/java/com/opensymphony/oscache/web/filter/CacheFilter.java

 /*
- * Copyright (c) 2002-2007 by OpenSymphony
+ * Copyright (c) 2002-2008 by OpenSymphony
  * All rights reserved.
  */
 package com.opensymphony.oscache.web.filter;
     public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
     public static final String HEADER_CACHE_CONTROL = "Cache-Control";
     public static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding";
+    public static final String HEADER_ETAG = "ETag";
 
     // Fragment parameter
     public static final int FRAGMENT_AUTODETECT = -1;
     public static final long EXPIRES_ON = 1;
     public static final long EXPIRES_TIME = -1;
     
+    // ETag parameter
+    public static final int ETAG_OFF = 0;
+    public static final int ETAG_WEAK = 1;
+    //public static final int ETAG_STRONG = 2;
+    
     // Cache Control
     public static final long MAX_AGE_NO_INIT = Long.MIN_VALUE;
     public static final long MAX_AGE_TIME = Long.MAX_VALUE;
     private int nocache = NOCACHE_OFF; // defines special no cache option for the requests - default is off
     private long lastModified = LAST_MODIFIED_INITIAL; // defines if the last-modified-header will be sent - default is intial setting
     private long expires = EXPIRES_ON; // defines if the expires-header will be sent - default is on
+    private int etag = ETAG_WEAK; // defines the type of the etag header - default is weak
     private long cacheControlMaxAge = -60; // defines which max-age in Cache-Control to be set - default is 60 seconds for max-age
     private ICacheKeyProvider cacheKeyProvider = this; // the provider of the cache key - default is the CacheFilter itselfs
     private ICacheGroupsProvider cacheGroupsProvider = this; // the provider of the cache groups - default is the CacheFilter itselfs
                     log.info("OSCache: New cache entry, cache stale or cache scope flushed for " + key);
                 }
 
-                CacheHttpServletResponseWrapper cacheResponse = new CacheHttpServletResponseWrapper((HttpServletResponse) response, fragmentRequest, time * 1000L, lastModified, expires, cacheControlMaxAge);
+                CacheHttpServletResponseWrapper cacheResponse = new CacheHttpServletResponseWrapper((HttpServletResponse) response, fragmentRequest, time * 1000L, lastModified, expires, cacheControlMaxAge, etag);
                 chain.doFilter(request, cacheResponse);
                 cacheResponse.flushBuffer();
 
             }
         }
 
+        // filter parameter expires
+        String etagParam = config.getInitParameter("etag");
+        if (etagParam != null) {
+            if ("off".equalsIgnoreCase(etagParam)) {
+                setETag(ETAG_OFF);
+            } else if ("weak".equalsIgnoreCase(etagParam)) {
+                setETag(ETAG_WEAK);
+            } else {
+                log.error("OSCache: Wrong value '" + etagParam + "' for init parameter 'etag', defaulting to 'weak'.");
+            }
+        }
+        
         // filter parameter Cache-Control
         String cacheControlMaxAgeParam = config.getInitParameter("max-age");
         if (cacheControlMaxAgeParam != null) {
      * Checks if the request was filtered before, so
      * guarantees to be executed once per request. You
      * can override this methods to define a more specific
-     * behaviour.
+     * behavior.
      *
      * @param request checks if the request was filtered before.
      * @return true if it is the first execution
     }
 
     /*
-     * isCacheableInternal gurarantees that the log information is correct.
+     * isCacheableInternal guarantees that the log information is correct.
      * 
      * @param request The servlet request
      * @return Returns a boolean indicating if the request can be cached or not.
 
     /**
      * isCacheable is a method allowing a subclass to decide if a request is
-     * cachable or not.
+     * cacheable or not.
      * 
      * @param request The servlet request
      * @return Returns a boolean indicating if the request can be cached or not.
     }
     
     /*
-     * isCacheableInternal gurarantees that the log information is correct.
+     * isCacheableInternal guarantees that the log information is correct.
      * 
      * @param cacheResponse the HTTP servlet response
      * @return Returns a boolean indicating if the response can be cached or not.
         final boolean cacheable = isCacheable(cacheResponse);
 
         if (log.isDebugEnabled()) {
-            log.debug("OSCache: the response " + ((cacheable) ? "is" : "is not") + " cachable.");
+            log.debug("OSCache: the response " + ((cacheable) ? "is" : "is not") + " cacheable.");
         }
         
         return cacheable;
 
     /**
      * isCacheable is a method allowing subclass to decide if a response is
-     * cachable or not.
+     * cacheable or not.
      * 
      * @param cacheResponse the HTTP servlet response
      * @return Returns a boolean indicating if the response can be cached or not.
     /**
      * <b>ICacheGroupsProvider</b> - Class implementing the interface <code>ICacheGroupsProvider</code>.
      * A developer can implement a method which provides cache groups based on the request, 
-     * the servlect cache administrator and cache. The parameter has to be not <code>null</code>.
+     * the servlet cache administrator and cache. The parameter has to be not <code>null</code>.
      *
      * @param cacheGroupsProvider the cacheGroupsProvider to set
      * @since 2.4
     }
 
     /**
+	 * @return the etag
+	 * @since 2.4.2
+	 */
+	public int getETag() {
+		return etag;
+	}
+
+	/**
+     * <b>etag</b> - defines if the Entity tag (ETag) HTTP header is sent in the response. Acceptable values are
+     * <code>ETAG_OFF</code> for don't sending the header, even it is set in the filter chain, 
+     * <code>ETAG_WEAK</code> (default) for generating a Weak ETag by concatenating the content length and the last modified time in milliseconds. 
+     * 
+     * @param etag the etag to set
+     * @since 2.4.2
+   	 */
+	public void setETag(int etag) {
+        if ((etag < ETAG_OFF) || (etag > ETAG_WEAK)) throw new IllegalArgumentException("ETag value out of range.");
+		this.etag = etag;
+	}
+
+	/**
      * @return the expiresRefreshPolicy
      * @since 2.4
      */

src/java/com/opensymphony/oscache/web/filter/CacheHttpServletResponseWrapper.java

 /*
- * Copyright (c) 2002-2003 by OpenSymphony
+ * Copyright (c) 2002-2008 by OpenSymphony
  * All rights reserved.
  */
 package com.opensymphony.oscache.web.filter;
     private long expires = CacheFilter.EXPIRES_ON;
     private long lastModified = CacheFilter.LAST_MODIFIED_INITIAL;
     private long cacheControl = -60;
+    private int etagOption = CacheFilter.ETAG_WEAK;
+    private String etag = null;
 
     /**
      * Constructor
      * @param response The servlet response
      */
     public CacheHttpServletResponseWrapper(HttpServletResponse response) {
-        this(response, false, Long.MAX_VALUE, CacheFilter.EXPIRES_ON, CacheFilter.LAST_MODIFIED_INITIAL, -60);
+        this(response, false, Long.MAX_VALUE, CacheFilter.EXPIRES_ON, CacheFilter.LAST_MODIFIED_INITIAL, -60, CacheFilter.ETAG_WEAK);
     }
 
     /**
      * @param lastModified defines if last modified header will be send, @see CacheFilter
      * @param expires defines if expires header will be send, @see CacheFilter
      * @param cacheControl defines if cache control header will be send, @see CacheFilter
+     * @param etagOption defines if the ETag header will be send, @see CacheFilter
      */
-    public CacheHttpServletResponseWrapper(HttpServletResponse response, boolean fragment, long time, long lastModified, long expires, long cacheControl) {
+    public CacheHttpServletResponseWrapper(HttpServletResponse response, boolean fragment, long time, long lastModified, long expires, long cacheControl, int etagOption) {
         super(response);
         result = new ResponseContent();
         this.fragment = fragment;
         this.expires = expires;
         this.lastModified = lastModified;
         this.cacheControl = cacheControl;
+        this.etagOption = etagOption;
         
-        // only set inital values for last modified and expires, when a complete page is cached
+        // only set initial values for last modified and expires, when a complete page is cached
         if (!fragment) {
             // setting a default last modified value based on object creation and remove the millis
             if (lastModified == CacheFilter.LAST_MODIFIED_INITIAL) {
             result.setContentEncoding(value);
         }
 
+        if (CacheFilter.HEADER_ETAG.equalsIgnoreCase(name)) {
+            result.setETag(value);
+        }
+
         super.setHeader(name, value);
     }
 
             result.setContentEncoding(value);
         }
 
+        if (CacheFilter.HEADER_ETAG.equalsIgnoreCase(name)) {
+            result.setETag(value);
+        }
+
         super.addHeader(name, value);
     }
 
     }
 
     public void flushBuffer() throws IOException {
+        // The weak ETag is content size + lastModified
+        if (etag == null) {
+            if (etagOption == CacheFilter.ETAG_WEAK) {
+        	    etag = "W/\"" + result.getSize() + "-" + result.getLastModified() + "\"";
+        	    result.setETag(etag);
+            }
+        } 
         super.flushBuffer();
         flush();
     }
 
     /**
      * Returns a boolean indicating if the response has been committed. 
-     * A commited response has already had its status code and headers written.
+     * A committed response has already had its status code and headers written.
      * 
      * @see javax.servlet.ServletResponseWrapper#isCommitted()
      */
         expires = CacheFilter.EXPIRES_ON;
         lastModified = CacheFilter.LAST_MODIFIED_INITIAL;
         cacheControl = -60;
+        etag = null;
         // time ?
         */
     }

src/java/com/opensymphony/oscache/web/filter/ResponseContent.java

     private long expires = Long.MAX_VALUE;
     private long lastModified = -1;
     private long maxAge = -60;
+    private String etag = null;
 
     public String getContentType() {
         return contentType;
         this.contentEncoding = contentEncoding;
     }
 
-    /**
+    public String getETag() {
+        return etag;
+    }
+
+    public void setETag(String etag) {
+        this.etag = etag;
+    }
+
+   /**
      * Set the Locale. We capture this so that when we serve this data from
      * cache, we can set the correct locale on the response.
      */
     }
 
     /**
-     * @return the expires date and time in miliseconds when the content will be stale
+     * @return the expires date and time in milliseconds when the content will be stale
      */
     public long getExpires() {
         return expires;
     }
 
     /**
-     * Sets the expires date and time in miliseconds.
-     * @param value time in miliseconds when the content will expire
+     * Sets the expires date and time in milliseconds.
+     * @param value time in milliseconds when the content will expire
      */
     public void setExpires(long value) {
         expires = value;
     }
 
 	/**
-	 * Returns the max age of the content in miliseconds. If expires header and cache control are
+	 * Returns the max age of the content in milliseconds. If expires header and cache control are
 	 * enabled both, both will be equal. 
-	 * @return the max age of the content in miliseconds, if -1 max-age is disabled
+	 * @return the max age of the content in milliseconds, if -1 max-age is disabled
 	 */
 	public long getMaxAge() {
 		return maxAge;
 	}
 
 	/**
-	 * Sets the max age date and time in miliseconds. If the parameter is -1, the max-age parameter
+	 * Sets the max age date and time in milliseconds. If the parameter is -1, the max-age parameter
 	 * won't be set by default in the Cache-Control header.
 	 * @param value sets the intial
 	 */
                     httpResponse.setDateHeader(CacheFilter.HEADER_LAST_MODIFIED, lastModified);
                 }
                 
+                // add the etag header
+                if (etag != null) {
+                    httpResponse.addHeader(CacheFilter.HEADER_ETAG, etag);
+                }
+                
                 // add the expires header
                 if (expires != Long.MAX_VALUE) {
                     httpResponse.setDateHeader(CacheFilter.HEADER_EXPIRES, expires);

src/webapp/filter/filterETagTest.jsp

+<%@ page import="java.util.*" %>
+<% boolean etag = "true".equalsIgnoreCase(request.getParameter("etag"));
+   if (etag) response.addHeader(com.opensymphony.oscache.web.filter.CacheFilter.HEADER_ETAG, "W/\"12345-67890\""); %>
+<head>
+<title>Filter ETag Test Page</title>
+<style type="text/css">
+body {font-family: Arial, Verdana, Geneva, Helvetica, sans-serif}
+</style>
+</head>
+<body>
+<a href="<%= request.getContextPath() %>/">Back to index</a><p>
+<hr>
+<b>Current Time</b>: <%= new Date() %><br>
+<b>Current Time Millis</b>: <%= System.currentTimeMillis() %><br>
+<b>ETag</b>: <%= etag %><br>
+</body>
+</html>

src/webapp/index.html

 <li><a href="cronTest.jsp">cronTest.jsp</a></li>
 <li><a href="silentTest.jsp">silentTest.jsp</a></li>
 <li><a href="filter/filterTest.jsp">filter/filterTest.jsp</a></li>
+<li><a href="filter/filterETagTest.jsp">filter/filterETagTest.jsp</a></li>
 <li><a href="filter2/filterTestDisableCacheOnMethods.jsp">filter2/filterTestDisableCacheOnMethods.jsp</a></li>
 <li><a href="oscacheTestMultipleTagNoKey.jsp">oscacheTestMultipleTagNoKey.jsp</a></li>
 <li><a href="cacheServlet?refreshPeriod=10">OSCache Test Servlet</a></li>
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.