<div>(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)</div><div><br></div>All, <div><div> 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?</div>
<div><br></div><div> I modified RequestHandlerFactory so it only checks for the last element of the path (after the last occurrence of '/')</div><div><br></div><div> 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 </div>
<div><br></div><div> 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. </div><div><br></div><div> Also, in the s:remote renderer, I am rendering %3B instead of ";" 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'm getting back a 200 OK, which is misleading. Thoughts?</div>
<div><br></div><div><div> Please let me know if I'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"><<a href="mailto:christian.bauer@gmail.com">christian.bauer@gmail.com</a>></span> wrote:<br>
<blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;">(I didn'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 "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?<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'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 <tt>GET</tt> or <tt>HEAD</tt> request. The typically usecase is as follows:<br>
* <p/><br>
* <pre><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>
* </pre><br>
* <p/><br>
* Note that the <tt>getLastModifiedTimestamp()</tt> method is only supplied for convenience; it may not<br>
* return what you expect as the "last modification timestamp" of the given resource. In many cases you'd<br>
* rather calculate that timestamp yourself.<br>
* <p/><br>
*/<br>
public abstract class ConditionalAbstractResource extends AbstractResource<br>
{<br>
<br>
public static final String HEADER_LAST_MODIFIED = "Last-Modified";<br>
public static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since";<br>
<br>
public static final String HEADER_ETAG = "ETag";<br>
public static final String HEADER_IF_NONE_MATCH = "If-None-Match";<br>
<br>
private static final LogProvider log = Logging.getLogProvider(ConditionalAbstractResource.class);<br>
<br>
/**<br>
* Validates the request headers <tt>If-Modified-Since</tt> and <tt>If-None-Match</tt> to determine<br>
* if a <tt>304 NOT MODIFIED</tt> response can be send. If that is the case, this method will automatically<br>
* send the response and return <tt>true</tt>. If condition validation fails, it will not change the<br>
* response and return <tt>false</tt>.<br>
* <p/><br>
* Note that both <tt>entityTag</tt> and <tt>lastModified</tt> arguments can be <tt>null</tt>. 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>
* <p/><br>
* In addition to responding with <tt>304 NOT MODIFIED</tt> when conditions match, this method will also, if<br>
* arguments are not <tt>null</tt>, 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>
* <p/><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 <tt>null</tt>, 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 <tt>null</tt>, 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 <tt>true</tt> if a <tt>304 NOT MODIFIED</tt> response status has been set, <tt>false</tt> 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("\"") || entityTag.startsWith("W/\"")) && !entityTag.endsWith("\""))<br>
{<br>
throw new IllegalArgumentException("Entity tag is not properly formatted (or quoted): " + 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 && modifiedSinceHeader != -1)<br>
{<br>
log.debug(HEADER_IF_NONE_MATCH + " and " + HEADER_IF_MODIFIED_SINCE + " must match");<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 && modifiedSinceValid)<br>
{<br>
log.debug(HEADER_IF_NONE_MATCH + " and " + HEADER_IF_MODIFIED_SINCE + " conditions match, sending 304");<br>
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);<br>
return true;<br>
}<br>
else<br>
{<br>
log.debug(HEADER_IF_NONE_MATCH + " and " + HEADER_IF_MODIFIED_SINCE + " conditions do not match, not sending 304");<br>
return false;<br>
}<br>
}<br>
<br>
if (noneMatchHeader != null && noneMatchValid)<br>
{<br>
log.debug(HEADER_IF_NONE_MATCH + " condition matches, sending 304");<br>
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);<br>
return true;<br>
}<br>
<br>
if (modifiedSinceHeader != -1 && modifiedSinceValid)<br>
{<br>
log.debug(HEADER_IF_MODIFIED_SINCE + " condition matches, sending 304");<br>
response.sendError(HttpServletResponse.SC_NOT_MODIFIED);<br>
return true;<br>
}<br>
<br>
log.debug("None of the cache conditions match, not sending 304");<br>
return false;<br>
}<br>
<br>
protected boolean isNoneMatchConditionValid(String noneMatchHeader, String entityTag)<br>
{<br>
if (noneMatchHeader.trim().equals("*"))<br>
{<br>
log.debug("Found * conditional request, hence current entity tag matches");<br>
return true;<br>
}<br>
String[] entityTagsArray = noneMatchHeader.trim().split(",");<br>
for (String requestTag : entityTagsArray)<br>
{<br>
if (requestTag.trim().equals(entityTag))<br>
{<br>
log.debug("Found matching entity tag in request");<br>
return true;<br>
}<br>
}<br>
log.debug("Resource has different entity tag than requested");<br>
return false;<br>
}<br>
<br>
protected boolean isModifiedSinceConditionValid(Long modifiedSinceHeader, Long lastModified)<br>
{<br>
if (lastModified <= modifiedSinceHeader)<br>
{<br>
log.debug("Resource has not been modified since requested timestamp");<br>
return true;<br>
}<br>
log.debug("Resource has been modified since requested timestamp");<br>
return false;<br>
}<br>
<br>
/**<br>
* Tries to get last modification timestamp of the resource by obtaining<br>
* a <tt>URLConnection</tt> 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 <tt>null</tt>,<br>
* a <tt>null</tt> 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 <tt>true</tt> 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 ? "W/\"" : "\"") + hash(hashSource, "UTF-8", "MD5") + "\"";<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 <tt>null</tt>,<br>
* a <tt>null</tt> will be returned.<br>
*<br>
* @param hashSource The string source for hashing.<br>
* @param weak Set to <tt>true</tt> 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 ? "W/\"" : "\"") + hash(hashSource, "MD5") + "\"";<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("0");<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>