Author: christian.bauer(a)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
+ */
+(a)Scope(ScopeType.APPLICATION)
+(a)Name("org.jboss.seam.web.cacheControlFilter")
+@Install(value = false, precedence = Install.BUILT_IN)
+@BypassInterceptors
+@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\"");
+
+ }
+}