<div>(My email has to be approved by a moderator, so if only Christian gets them, it&#39;s because the email hasn&#39;t gotten approved yet)</div><div><br></div>All, <div><div>  Here&#39;s my first stab at Seam remoting interface.js caching (patch attached)... Should I be attaching this to a thread in a JIRA somewhere?</div>
<div><br></div><div>  I modified RequestHandlerFactory so it only checks for the last element of the path (after the last occurrence of &#39;/&#39;)</div><div><br></div><div>  This allows for /remoting/arbitrary-string/interface.js to be handled by the handler for /interface.js and shouldn&#39;t break the handlers for /subscription and /poll </div>
<div><br></div><div>  InterfaceGenerator checks to see that the path is valid, and extracts component names from the path. I haven&#39;t done any HTTP caching stuff yet. </div><div><br></div><div>  Also, in the s:remote renderer, I am rendering %3B instead of &quot;;&quot; because tomcat requires the semicolons to be urlencoded. (Ref: <a href="https://issues.apache.org/bugzilla/show_bug.cgi?id=30535#c3">https://issues.apache.org/bugzilla/show_bug.cgi?id=30535#c3</a>)</div>
<div><br></div><div>  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&#39;m getting back a 200 OK, which is misleading. Thoughts?</div>
<div><br></div><div><div>  Please let me know if I&#39;m on the right track re: code.</div><div><br></div></div><div>Thanks,</div><div><br></div><div>Ashish</div><div><div><div><br><div class="gmail_quote">On Fri, Jun 26, 2009 at 2:59 PM, Christian Bauer <span dir="ltr">&lt;<a href="mailto:christian.bauer@gmail.com">christian.bauer@gmail.com</a>&gt;</span> wrote:<br>
<blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">(I didn&#39;t get this e-mail, please reply to the list in future.)<div class="im"><br>
<br>
<blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">
Christian: In addition to support for &quot;304 Not Modified&quot;, are you planning to support http cache headers like either &quot;Cache-Control&quot; or &quot;Expires&quot;? (that way, the browser doesn&#39;t even perform a server roundtrip). And can I find a copy of your work so far in svn to serve as a guide?<br>

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