[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