[seam-commits] Seam SVN: r11419 - in branches/community/Seam_2_2: src/main/org/jboss/seam and 3 other directories.

seam-commits at lists.jboss.org seam-commits at lists.jboss.org
Tue Aug 25 04:13:36 EDT 2009


Author: christian.bauer at jboss.com
Date: 2009-08-25 04:13:36 -0400 (Tue, 25 Aug 2009)
New Revision: 11419

Added:
   branches/community/Seam_2_2/src/main/org/jboss/seam/web/CacheControlFilter.java
   branches/community/Seam_2_2/src/main/org/jboss/seam/web/ConditionalAbstractResource.java
   branches/community/Seam_2_2/src/test/unit/org/jboss/seam/test/unit/web/ConditionalRequestTest.java
Modified:
   branches/community/Seam_2_2/doc/Seam_Reference_Guide/en-US/Configuration.xml
   branches/community/Seam_2_2/src/main/org/jboss/seam/web-2.2.xsd
   branches/community/Seam_2_2/src/main/org/jboss/seam/web/AbstractResource.java
   branches/community/Seam_2_2/src/test/unit/org/jboss/seam/test/unit/testng.xml
Log:
JBSEAM-4383, infrastructure for HTTP optimization

Modified: branches/community/Seam_2_2/doc/Seam_Reference_Guide/en-US/Configuration.xml
===================================================================
--- branches/community/Seam_2_2/doc/Seam_Reference_Guide/en-US/Configuration.xml	2009-08-24 13:41:30 UTC (rev 11418)
+++ branches/community/Seam_2_2/doc/Seam_Reference_Guide/en-US/Configuration.xml	2009-08-25 08:13:36 UTC (rev 11419)
@@ -374,8 +374,36 @@
 
             </sect3>
 
-            
             <sect3>
+                <title>Enabling HTTP cache-control headers</title>
+                <para>
+                    Seam does <emphasis>not</emphasis> automatically add <literal>cache-control</literal> HTTP headers to
+                    any resources served by the Seam resource servlet, or directly from your view directory by the servlet
+                    container. This means that your images, Javascript and CSS files, and resource representations from
+                    Seam resource servlet such as Seam Remoting Javascript interfaces are usually not cached by the browser.
+                    This is convenient in development but should be changed in production when optimizing the application.
+                </para>
+
+                <para>
+                    You can configure a Seam filter to enable automatic addition of <literal>cache-control</literal> headers
+                    depending on the requested URI in <literal>components.xml</literal>:
+                </para>
+
+                <programlisting role="XML"><![CDATA[<web:cache-control-filter name="commonTypesCacheControlFilter"
+                          regex-url-pattern=".*(\.gif|\.png|\.jpg|\.jpeg|\.css|\.js)"
+                          value="max-age=86400"/> <!-- 1 day -->
+
+<web:cache-control-filter name="anotherCacheControlFilter"
+                          url-pattern="/my/cachable/resources/*"
+                          value="max-age=432000"/> <!-- 5 days -->]]></programlisting>
+
+                <para>
+                    You do not have to name the filters unless you have more than one filter enabled.
+                </para>
+
+            </sect3>
+
+            <sect3>
                 <title>Adding custom filters</title>
                 <para> Seam can install your filters for you, allowing you to specify <emphasis>where</emphasis> in the
                     chain your filter is placed (the servlet specification doesn't provide a well defined order if you

Modified: branches/community/Seam_2_2/src/main/org/jboss/seam/web/AbstractResource.java
===================================================================
--- branches/community/Seam_2_2/src/main/org/jboss/seam/web/AbstractResource.java	2009-08-24 13:41:30 UTC (rev 11418)
+++ branches/community/Seam_2_2/src/main/org/jboss/seam/web/AbstractResource.java	2009-08-25 08:13:36 UTC (rev 11419)
@@ -1,42 +1,177 @@
 package org.jboss.seam.web;
 
-import java.io.IOException;
-
 import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.zip.GZIPOutputStream;
 
 /**
- * Superclass of Seam components that serve up 
- * "resources" to the client via the Seam 
+ * Superclass of Seam components that serve up
+ * "resources" to the client via the Seam
  * resource servlet. Note that since a filter is
  * potentially called outside of a set of Seam
- * contexts, it is not a true Seam component. 
+ * contexts, it is not a true Seam component.
  * However, we are able to reuse the functionality
- * for component scanning, installation and 
+ * for component scanning, installation and
  * configuration for filters. All resources
  * must extend this class.
- * 
+ *
  * @author Shane Bryzak
  *
  */
 public abstract class AbstractResource
 {
    private ServletContext context;
-   
+
    protected ServletContext getServletContext()
    {
       return context;
    }
-   
+
    public void setServletContext(ServletContext context)
    {
       this.context = context;
    }
-         
+
    public abstract void getResource(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException;
-   
+
    public abstract String getResourcePath();
+
+   protected OutputStream selectOutputStream(HttpServletRequest request, HttpServletResponse response)
+         throws IOException
+   {
+
+      String acceptEncoding = request.getHeader("Accept-Encoding");
+      String mimeType = response.getContentType();
+
+      if (isGzipEnabled()
+            && acceptEncoding != null
+            && acceptEncoding.length() > 0
+            && acceptEncoding.indexOf("gzip") > -1
+            && isCompressedMimeType(mimeType))
+      {
+         return new GZIPResponseStream(response);
+      }
+      else
+      {
+         return response.getOutputStream();
+      }
+   }
+
+   protected boolean isCompressedMimeType(String mimeType)
+   {
+      return mimeType.matches("text/.+");
+   }
+
+   protected boolean isGzipEnabled()
+   {
+      return true;
+   }
+
+   /*
+    * Copyright 2004-2008 the original author or authors.
+    *
+    * 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.
+    *
+    * @See org/springframework/js/resource/ResourceServlet.java
+    */
+   private class GZIPResponseStream extends ServletOutputStream
+   {
+
+      private ByteArrayOutputStream byteStream = null;
+
+      private GZIPOutputStream gzipStream = null;
+
+      private boolean closed = false;
+
+      private HttpServletResponse response = null;
+
+      private ServletOutputStream servletStream = null;
+
+      public GZIPResponseStream(HttpServletResponse response) throws IOException
+      {
+         super();
+         closed = false;
+         this.response = response;
+         this.servletStream = response.getOutputStream();
+         byteStream = new ByteArrayOutputStream();
+         gzipStream = new GZIPOutputStream(byteStream);
+      }
+
+      public void close() throws IOException
+      {
+         if (closed)
+         {
+            throw new IOException("This output stream has already been closed");
+         }
+         gzipStream.finish();
+
+         byte[] bytes = byteStream.toByteArray();
+
+         response.setContentLength(bytes.length);
+         response.addHeader("Content-Encoding", "gzip");
+         servletStream.write(bytes);
+         servletStream.flush();
+         servletStream.close();
+         closed = true;
+      }
+
+      public void flush() throws IOException
+      {
+         if (closed)
+         {
+            throw new IOException("Cannot flush a closed output stream");
+         }
+         gzipStream.flush();
+      }
+
+      public void write(int b) throws IOException
+      {
+         if (closed)
+         {
+            throw new IOException("Cannot write to a closed output stream");
+         }
+         gzipStream.write((byte) b);
+      }
+
+      public void write(byte b[]) throws IOException
+      {
+         write(b, 0, b.length);
+      }
+
+      public void write(byte b[], int off, int len) throws IOException
+      {
+         if (closed)
+         {
+            throw new IOException("Cannot write to a closed output stream");
+         }
+         gzipStream.write(b, off, len);
+      }
+
+      public boolean closed()
+      {
+         return (this.closed);
+      }
+
+      public void reset()
+      {
+         // noop
+      }
+   }
 }

Added: branches/community/Seam_2_2/src/main/org/jboss/seam/web/CacheControlFilter.java
===================================================================
--- branches/community/Seam_2_2/src/main/org/jboss/seam/web/CacheControlFilter.java	                        (rev 0)
+++ branches/community/Seam_2_2/src/main/org/jboss/seam/web/CacheControlFilter.java	2009-08-25 08:13:36 UTC (rev 11419)
@@ -0,0 +1,64 @@
+package org.jboss.seam.web;
+
+import org.jboss.seam.ScopeType;
+import org.jboss.seam.log.Logging;
+import org.jboss.seam.log.LogProvider;
+import org.jboss.seam.annotations.Install;
+import org.jboss.seam.annotations.Name;
+import org.jboss.seam.annotations.Scope;
+import org.jboss.seam.annotations.intercept.BypassInterceptors;
+import org.jboss.seam.annotations.web.Filter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Provides automatic addition of cache-control HTTP headers to matching resource responses.
+ *
+ * @author Christian Bauer
+ */
+ at Scope(ScopeType.APPLICATION)
+ at Name("org.jboss.seam.web.cacheControlFilter")
+ at Install(value = false, precedence = Install.BUILT_IN)
+ at BypassInterceptors
+ at Filter(within = "org.jboss.seam.web.exceptionFilter")
+public class CacheControlFilter extends AbstractFilter
+{
+
+   private static final LogProvider log = Logging.getLogProvider(CacheControlFilter.class);
+
+   private String value;
+
+   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+         throws IOException, ServletException
+   {
+
+      HttpServletRequest httpRequest = (HttpServletRequest) request;
+
+      if (isMappedToCurrentRequestPath(request))
+      {
+         log.debug("Applying Cache-Control HTTP header for resource '"
+               + httpRequest.getRequestURI() + "': " + getValue());
+
+         HttpServletResponse httpResponse = (HttpServletResponse) response;
+         httpResponse.setHeader("Cache-Control", getValue());
+      }
+
+      chain.doFilter(request, response);
+   }
+
+   public String getValue()
+   {
+      return value;
+   }
+
+   public void setValue(String value)
+   {
+      this.value = value;
+   }
+}

Added: branches/community/Seam_2_2/src/main/org/jboss/seam/web/ConditionalAbstractResource.java
===================================================================
--- branches/community/Seam_2_2/src/main/org/jboss/seam/web/ConditionalAbstractResource.java	                        (rev 0)
+++ branches/community/Seam_2_2/src/main/org/jboss/seam/web/ConditionalAbstractResource.java	2009-08-25 08:13:36 UTC (rev 11419)
@@ -0,0 +1,281 @@
+package org.jboss.seam.web;
+
+import org.jboss.seam.log.LogProvider;
+import org.jboss.seam.log.Logging;
+import org.jboss.seam.util.Resources;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.net.URLConnection;
+import java.net.URL;
+import java.lang.management.ManagementFactory;
+
+/**
+ * Subclass this resource if you want to be able to send the right response automatically to
+ * any conditional <tt>GET</tt> or <tt>HEAD</tt> request. The typically usecase is as follows:
+ * <p/>
+ * <pre>
+ * public class MyResource extends ConditionalAbstractResource {
+ *
+ *     public void getResource(final HttpServletRequest request, final HttpServletResponse response) {
+ *         String resourceVersion = ... // Calculate current state as string
+ *         or
+ *         byte[] resourceVersion = ... // Calculate current state as bytes
+ *
+ *         String resourcePath = ... // Get the relative (to servlet) path of the requested resource
+ *
+ *         if ( !sendConditional(request,
+ *                              response,
+ *                              createdEntityTag(resourceVersion, false),
+ *                              getLastModifiedTimestamp(resourcePath) ) {
+ * 
+ *             // Send the regular resource representation with 200 OK etc.
+ *         }
+ *     }
+ * }
+ * </pre>
+ * <p/>
+ * Note that the <tt>getLastModifiedTimestamp()</tt> method is only supplied for convenience; it may not
+ * return what you expect as the "last modification timestamp" of the given resource. In many cases you'd
+ * rather calculate that timestamp yourself.
+ * <p/>
+ *
+ * @author Christian Bauer
+ */
+public abstract class ConditionalAbstractResource extends AbstractResource
+{
+
+   public static final String HEADER_LAST_MODIFIED = "Last-Modified";
+   public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";
+
+   public static final String HEADER_ETAG = "ETag";
+   public static final String HEADER_IF_NONE_MATCH = "If-None-Match";
+
+   private static final LogProvider log = Logging.getLogProvider(ConditionalAbstractResource.class);
+
+   /**
+    * Validates the request headers <tt>If-Modified-Since</tt> and <tt>If-None-Match</tt> to determine
+    * if a <tt>304 NOT MODIFIED</tt> response can be send. If that is the case, this method will automatically
+    * send the response and return <tt>true</tt>. If condition validation fails, it will not change the
+    * response and return <tt>false</tt>.
+    * <p/>
+    * Note that both <tt>entityTag</tt> and <tt>lastModified</tt> arguments can be <tt>null</tt>. The validation
+    * procedure and the outcome depends on what the client requested. If the client requires that both entity tags and
+    * modification timestamps be validated, both arguments must be supplied to the method and they must match, for
+    * a 304 response to be send.
+    * <p/>
+    * In addition to responding with <tt>304 NOT MODIFIED</tt> when conditions match, this method will also, if
+    * arguments are not <tt>null</tt>, send the right entity tag and last modification timestamps with the response,
+    * so that future requests from the client can be made conditional.
+    * <p/>
+    *
+    * @param request         The usual HttpServletRequest for header retrieval.
+    * @param response        The usual HttpServletResponse for header manipulation.
+    * @param entityTag       An entity tag (weak or strong, in doublequotes), typically produced by hashing the content
+    *                        of the resource representation. If <tt>null</tt>, no entity tag will be send and if
+    *                        validation is requested by the client, no match for a NOT MODIFIED response will be possible.
+    * @param lastModified    The timestamp in number of milliseconds since unix epoch when the resource was
+    *                        last modified. If <tt>null</tt>, no last modification timestamp will be send  and if
+    *                        validation is requested by the client, no match for a NOT MODIFIED response will be possible.
+    * @return <tt>true</tt> if a <tt>304 NOT MODIFIED</tt> response status has been set, <tt>false</tt> if requested
+    *         conditions were invalid given the current state of the resource.
+    * @throws IOException If setting the response status failed.
+    */
+   public boolean sendConditional(HttpServletRequest request,
+                                  HttpServletResponse response,
+                                  String entityTag, Long lastModified) throws IOException
+   {
+
+      String noneMatchHeader = request.getHeader(HEADER_IF_NONE_MATCH);
+      Long modifiedSinceHeader = request.getDateHeader(HEADER_IF_MODIFIED_SINCE); // Careful, returns -1 instead of null!
+
+      boolean noneMatchValid = false;
+      if (entityTag != null)
+      {
+
+         if (! (entityTag.startsWith("\"") || entityTag.startsWith("W/\"")) && !entityTag.endsWith("\""))
+         {
+            throw new IllegalArgumentException("Entity tag is not properly formatted (or quoted): " + entityTag);
+         }
+
+         // Always send an entity tag with the response
+         response.setHeader(HEADER_ETAG, entityTag);
+
+         if (noneMatchHeader != null)
+         {
+            noneMatchValid = isNoneMatchConditionValid(noneMatchHeader, entityTag);
+         }
+      }
+
+      boolean modifiedSinceValid = false;
+      if (lastModified != null)
+      {
+
+         // Always send the last modified timestamp with the response
+         response.setDateHeader(HEADER_LAST_MODIFIED, lastModified);
+
+         if (modifiedSinceHeader != -1)
+         {
+            modifiedSinceValid = isModifiedSinceConditionValid(modifiedSinceHeader, lastModified);
+         }
+
+      }
+
+      if (noneMatchHeader != null && modifiedSinceHeader != -1)
+      {
+         log.debug(HEADER_IF_NONE_MATCH + " and " + HEADER_IF_MODIFIED_SINCE + " must match");
+
+         // If both are received, we must not return 304 unless doing so is consistent with both header fields in the request!
+         if (noneMatchValid && modifiedSinceValid)
+         {
+            log.debug(HEADER_IF_NONE_MATCH + " and " + HEADER_IF_MODIFIED_SINCE + " conditions match, sending 304");
+            response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
+            return true;
+         }
+         else
+         {
+            log.debug(HEADER_IF_NONE_MATCH + " and " + HEADER_IF_MODIFIED_SINCE + " conditions do not match, not sending 304");
+            return false;
+         }
+      }
+
+      if (noneMatchHeader != null && noneMatchValid)
+      {
+         log.debug(HEADER_IF_NONE_MATCH + " condition matches, sending 304");
+         response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
+         return true;
+      }
+
+      if (modifiedSinceHeader != -1 && modifiedSinceValid)
+      {
+         log.debug(HEADER_IF_MODIFIED_SINCE + " condition matches, sending 304");
+         response.sendError(HttpServletResponse.SC_NOT_MODIFIED);
+         return true;
+      }
+
+      log.debug("None of the cache conditions match, not sending 304");
+      return false;
+   }
+
+   protected boolean isNoneMatchConditionValid(String noneMatchHeader, String entityTag)
+   {
+      if (noneMatchHeader.trim().equals("*"))
+      {
+         log.debug("Found * conditional request, hence current entity tag matches");
+         return true;
+      }
+      String[] entityTagsArray = noneMatchHeader.trim().split(",");
+      for (String requestTag : entityTagsArray)
+      {
+         if (requestTag.trim().equals(entityTag))
+         {
+            log.debug("Found matching entity tag in request");
+            return true;
+         }
+      }
+      log.debug("Resource has different entity tag than requested");
+      return false;
+   }
+
+   protected boolean isModifiedSinceConditionValid(Long modifiedSinceHeader, Long lastModified)
+   {
+      if (lastModified <= modifiedSinceHeader)
+      {
+         log.debug("Resource has not been modified since requested timestamp");
+         return true;
+      }
+      log.debug("Resource has been modified since requested timestamp");
+      return false;
+   }
+
+   /**
+    * Tries to get last modification timestamp of the resource by obtaining
+    * a <tt>URLConnection</tt> to the file in the filesystem or JAR.
+    *
+    * @param resourcePath The relative (to the servlet) resource path.
+    * @return Either the last modified filestamp or if an error occurs, the JVM system startup timestamp.
+    */
+   protected Long getLastModifiedTimestamp(String resourcePath)
+   {
+      try
+      {
+         // Try to load it from filesystem or JAR through URLConnection
+         URL resourceURL = Resources.getResource(resourcePath, getServletContext());
+         if (resourceURL == null)
+         {
+            // Fall back to startup time of the JVM
+            return ManagementFactory.getRuntimeMXBean().getStartTime();
+         }
+         URLConnection resourceConn = resourceURL.openConnection();
+         return resourceConn.getLastModified();
+      }
+      catch (Exception ex)
+      {
+         // Fall back to startup time of the JVM
+         return ManagementFactory.getRuntimeMXBean().getStartTime();
+      }
+   }
+
+   /**
+    * Generates a (globally) unique identifier of the current state of the resource. The string will be
+    * hashed with MD5 and the hash result is then formatted before it is returned. If <tt>null</tt>,
+    * a <tt>null</tt> will be returned.
+    *
+    * @param hashSource The string source for hashing or the already hashed (strong or weak) entity tag.
+    * @param weak       Set to <tt>true</tt> if you want a weak entity tag.
+    * @return The hashed and formatted entity tag result.
+    */
+   protected String createEntityTag(String hashSource, boolean weak)
+   {
+      if (hashSource == null) return null;
+      return (weak ? "W/\"" : "\"") + hash(hashSource, "UTF-8", "MD5") + "\"";
+   }
+
+   /**
+    * Generates a (globally) unique identifier of the current state of the resource. The bytes will be
+    * hashed with MD5 and the hash result is then formatted before it is returned. If <tt>null</tt>,
+    * a <tt>null</tt> will be returned.
+    *
+    * @param hashSource The string source for hashing.
+    * @param weak       Set to <tt>true</tt> if you want a weak entity tag.
+    * @return The hashed and formatted entity tag result.
+    */
+   protected String createEntityTag(byte[] hashSource, boolean weak)
+   {
+      if (hashSource == null) return null;
+      return (weak ? "W/\"" : "\"") + hash(hashSource, "MD5") + "\"";
+   }
+
+   protected String hash(String text, String charset, String algorithm)
+   {
+      try
+      {
+         return hash(text.getBytes(charset), algorithm);
+      }
+      catch (Exception e)
+      {
+         throw new RuntimeException(e);
+      }
+   }
+
+   protected String hash(byte[] bytes, String algorithm)
+   {
+      try
+      {
+         MessageDigest md = MessageDigest.getInstance(algorithm);
+         md.update(bytes);
+         BigInteger number = new BigInteger(1, md.digest());
+         StringBuffer sb = new StringBuffer("0");
+         sb.append(number.toString(16));
+         return sb.toString();
+      }
+      catch (Exception e)
+      {
+         throw new RuntimeException(e);
+      }
+   }
+
+}

Modified: branches/community/Seam_2_2/src/main/org/jboss/seam/web-2.2.xsd
===================================================================
--- branches/community/Seam_2_2/src/main/org/jboss/seam/web-2.2.xsd	2009-08-24 13:41:30 UTC (rev 11418)
+++ branches/community/Seam_2_2/src/main/org/jboss/seam/web-2.2.xsd	2009-08-25 08:13:36 UTC (rev 11419)
@@ -185,4 +185,15 @@
         </xs:attribute>
     </xs:attributeGroup>
    
+    <xs:element name="cache-control-filter">
+        <xs:annotation>
+            <xs:documentation>Sets the HTTP Cache-Control header</xs:documentation>
+        </xs:annotation>
+        <xs:complexType mixed="true">
+            <xs:attributeGroup ref="components:attlist.component"/>
+            <xs:attributeGroup ref="web:attlist.filter"/>
+            <xs:attribute name="value" type="components:string"/>
+        </xs:complexType>
+    </xs:element>
+
 </xs:schema>

Modified: branches/community/Seam_2_2/src/test/unit/org/jboss/seam/test/unit/testng.xml
===================================================================
--- branches/community/Seam_2_2/src/test/unit/org/jboss/seam/test/unit/testng.xml	2009-08-24 13:41:30 UTC (rev 11418)
+++ branches/community/Seam_2_2/src/test/unit/org/jboss/seam/test/unit/testng.xml	2009-08-25 08:13:36 UTC (rev 11419)
@@ -70,6 +70,7 @@
    <test name="Seam Unit Tests: Resources and i8ln">
      <classes>
         <class name="org.jboss.seam.test.unit.InterpolatorTest"/>
+        <class name="org.jboss.seam.test.unit.web.ConditionalRequestTest" />
      </classes>
    </test>
 

Added: branches/community/Seam_2_2/src/test/unit/org/jboss/seam/test/unit/web/ConditionalRequestTest.java
===================================================================
--- branches/community/Seam_2_2/src/test/unit/org/jboss/seam/test/unit/web/ConditionalRequestTest.java	                        (rev 0)
+++ branches/community/Seam_2_2/src/test/unit/org/jboss/seam/test/unit/web/ConditionalRequestTest.java	2009-08-25 08:13:36 UTC (rev 11419)
@@ -0,0 +1,228 @@
+package org.jboss.seam.test.unit.web;
+
+import org.testng.annotations.Test;
+import org.testng.Assert;
+import static org.testng.Assert.assertEquals;
+import org.jboss.seam.mock.MockHttpSession;
+import org.jboss.seam.mock.EnhancedMockHttpServletRequest;
+import org.jboss.seam.mock.EnhancedMockHttpServletResponse;
+import org.jboss.seam.web.ConditionalAbstractResource;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.Date;
+
+/**
+ * @author Christian Bauer
+ *
+ */
+public class ConditionalRequestTest
+{
+   @Test
+   public void testNotModifiedOnlyETag() throws Exception
+   {
+
+      HttpSession session = new MockHttpSession();
+      EnhancedMockHttpServletRequest request = new EnhancedMockHttpServletRequest(session);
+      EnhancedMockHttpServletResponse response = new EnhancedMockHttpServletResponse();
+
+      request.addHeader(ConditionalAbstractResource.HEADER_IF_NONE_MATCH, "\"1234\", \"5678\"");
+
+      ConditionalAbstractResource resource = new ConditionalAbstractResource()
+      {
+         public void getResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+         {
+            if (!sendConditional(request, response, "\"5678\"", null))
+            {
+               response.sendError(HttpServletResponse.SC_OK);
+            }
+         }
+
+         public String getResourcePath()
+         {
+            return null;
+         }
+      };
+
+      resource.getResource(request, response);
+
+      assertEquals(response.getStatus(), HttpServletResponse.SC_NOT_MODIFIED);
+      assertEquals(response.getHeader(ConditionalAbstractResource.HEADER_ETAG), "\"5678\"");
+
+   }
+
+   @Test
+   public void testModifiedOnlyETag() throws Exception
+   {
+
+      HttpSession session = new MockHttpSession();
+      EnhancedMockHttpServletRequest request = new EnhancedMockHttpServletRequest(session);
+      EnhancedMockHttpServletResponse response = new EnhancedMockHttpServletResponse();
+
+      request.addHeader(ConditionalAbstractResource.HEADER_IF_NONE_MATCH, "\"123\", \"456\"");
+
+      ConditionalAbstractResource resource = new ConditionalAbstractResource()
+      {
+         public void getResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+         {
+            if (!sendConditional(request, response, "\"5678\"", null))
+            {
+               response.sendError(HttpServletResponse.SC_OK);
+            }
+         }
+
+         public String getResourcePath()
+         {
+            return null;
+         }
+      };
+
+      resource.getResource(request, response);
+
+      assertEquals(response.getStatus(), HttpServletResponse.SC_OK);
+      assertEquals(response.getHeader(ConditionalAbstractResource.HEADER_ETAG), "\"5678\"");
+   }
+
+   @Test
+   public void testNotModifiedOnlyLastModified() throws Exception
+   {
+
+      HttpSession session = new MockHttpSession();
+      EnhancedMockHttpServletRequest request = new EnhancedMockHttpServletRequest(session);
+      EnhancedMockHttpServletResponse response = new EnhancedMockHttpServletResponse();
+
+      final Long currentTime = new Date().getTime();
+      request.addHeader(ConditionalAbstractResource.HEADER_IF_MODIFIED_SINCE, currentTime);
+
+      ConditionalAbstractResource resource = new ConditionalAbstractResource()
+      {
+         public void getResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+         {
+            if (!sendConditional(request, response, null, currentTime))
+            {
+               response.sendError(HttpServletResponse.SC_OK);
+            }
+         }
+
+         public String getResourcePath()
+         {
+            return null;
+         }
+      };
+
+      resource.getResource(request, response);
+
+      assertEquals(response.getStatus(), HttpServletResponse.SC_NOT_MODIFIED);
+      assertEquals(response.getHeader(ConditionalAbstractResource.HEADER_LAST_MODIFIED), currentTime);
+
+   }
+
+   @Test
+   public void testModifiedOnlyLastModified() throws Exception
+   {
+
+      HttpSession session = new MockHttpSession();
+      EnhancedMockHttpServletRequest request = new EnhancedMockHttpServletRequest(session);
+      EnhancedMockHttpServletResponse response = new EnhancedMockHttpServletResponse();
+
+      final Long currentTime = new Date().getTime();
+      request.addHeader(ConditionalAbstractResource.HEADER_IF_MODIFIED_SINCE, currentTime);
+
+      ConditionalAbstractResource resource = new ConditionalAbstractResource()
+      {
+         public void getResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+         {
+            if (!sendConditional(request, response, null, currentTime + 5000))
+            {
+               response.sendError(HttpServletResponse.SC_OK);
+            }
+         }
+
+         public String getResourcePath()
+         {
+            return null;
+         }
+      };
+
+      resource.getResource(request, response);
+
+      assertEquals(response.getStatus(), HttpServletResponse.SC_OK);
+      assertEquals(response.getHeader(ConditionalAbstractResource.HEADER_LAST_MODIFIED), currentTime + 5000);
+
+   }
+
+   @Test
+   public void testNotModifiedETagLastModified() throws Exception
+   {
+
+      HttpSession session = new MockHttpSession();
+      EnhancedMockHttpServletRequest request = new EnhancedMockHttpServletRequest(session);
+      EnhancedMockHttpServletResponse response = new EnhancedMockHttpServletResponse();
+
+      final Long currentTime = new Date().getTime();
+      request.addHeader(ConditionalAbstractResource.HEADER_IF_MODIFIED_SINCE, currentTime);
+      request.addHeader(ConditionalAbstractResource.HEADER_IF_NONE_MATCH, "\"1234\", \"5678\"");
+
+      ConditionalAbstractResource resource = new ConditionalAbstractResource()
+      {
+         public void getResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+         {
+            if (!sendConditional(request, response, "\"5678\"", currentTime))
+            {
+               response.sendError(HttpServletResponse.SC_OK);
+            }
+         }
+
+         public String getResourcePath()
+         {
+            return null;
+         }
+      };
+
+      resource.getResource(request, response);
+
+      assertEquals(response.getStatus(), HttpServletResponse.SC_NOT_MODIFIED);
+      assertEquals(response.getHeader(ConditionalAbstractResource.HEADER_LAST_MODIFIED), currentTime);
+      assertEquals(response.getHeader(ConditionalAbstractResource.HEADER_ETAG), "\"5678\"");
+
+   }
+
+   @Test
+   public void testModifiedETagLastModified() throws Exception
+   {
+
+      HttpSession session = new MockHttpSession();
+      EnhancedMockHttpServletRequest request = new EnhancedMockHttpServletRequest(session);
+      EnhancedMockHttpServletResponse response = new EnhancedMockHttpServletResponse();
+
+      final Long currentTime = new Date().getTime();
+      request.addHeader(ConditionalAbstractResource.HEADER_IF_MODIFIED_SINCE, currentTime);
+      request.addHeader(ConditionalAbstractResource.HEADER_IF_NONE_MATCH, "\"1234\", \"5678\"");
+
+      ConditionalAbstractResource resource = new ConditionalAbstractResource()
+      {
+         public void getResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
+         {
+            if (!sendConditional(request, response, "\"5678\"", currentTime + 5000))
+            {
+               response.sendError(HttpServletResponse.SC_OK);
+            }
+         }
+
+         public String getResourcePath()
+         {
+            return null;
+         }
+      };
+
+      resource.getResource(request, response);
+
+      assertEquals(response.getStatus(), HttpServletResponse.SC_OK);
+      assertEquals(response.getHeader(ConditionalAbstractResource.HEADER_LAST_MODIFIED), currentTime + 5000);
+      assertEquals(response.getHeader(ConditionalAbstractResource.HEADER_ETAG), "\"5678\"");
+
+   }
+}



More information about the seam-commits mailing list