[seam-dev] Caching of Seam Remoting Interface Components
Ashish Tonse
ashish.tonse at gmail.com
Wed Jul 1 00:32:36 EDT 2009
(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:
https://issues.apache.org/bugzilla/show_bug.cgi?id=30535#c3)
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 at 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 at lists.jboss.org
> https://lists.jboss.org/mailman/listinfo/seam-dev
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.jboss.org/pipermail/seam-dev/attachments/20090701/887a1036/attachment.html
-------------- next part --------------
A non-text attachment was scrubbed...
Name: restful-remoting.diff
Type: application/octet-stream
Size: 3471 bytes
Desc: not available
Url : http://lists.jboss.org/pipermail/seam-dev/attachments/20090701/887a1036/attachment.obj
More information about the seam-dev
mailing list