(My email has to be approved by a moderator, so if only Christian gets them,
it's because the email hasn't gotten approved yet)
All, Here's my first stab at Seam remoting interface.js caching (patch
attached)... Should I be attaching this to a thread in a JIRA somewhere?
I modified RequestHandlerFactory so it only checks for the last element of
the path (after the last occurrence of '/')
This allows for /remoting/arbitrary-string/interface.js to be handled by
the handler for /interface.js and shouldn't break the handlers for
/subscription and /poll
InterfaceGenerator checks to see that the path is valid, and extracts
component names from the path. I haven't done any HTTP caching stuff yet.
Also, in the s:remote renderer, I am rendering %3B instead of ";" because
tomcat requires the semicolons to be urlencoded. (Ref:
)
Regarding error messages, I think it would be more appropriate to send
either a 404 (component not found) or 401 (Bad Request) or plain 500 status
code when an invalid component or path is specified. Right now, I'm getting
back a 200 OK, which is misleading. Thoughts?
Please let me know if I'm on the right track re: code.
Thanks,
Ashish
On Fri, Jun 26, 2009 at 2:59 PM, Christian Bauer
<christian.bauer(a)gmail.com>wrote:
(I didn't get this e-mail, please reply to the list in future.)
Christian: In addition to support for "304 Not Modified", are you planning
>> to support http cache headers like either "Cache-Control" or
"Expires"?
>> (that way, the browser doesn't even perform a server roundtrip). And can I
>> find a copy of your work so far in svn to serve as a guide?
>>
>
Conditional request handling without cache control is kind of useless, so
the answer to the first question is Yes.
I'll commit something next week, this is the finished conditional request
part:
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/>
*/
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);
}
}
}
_______________________________________________
seam-dev mailing list
seam-dev(a)lists.jboss.org
https://lists.jboss.org/mailman/listinfo/seam-dev