/*
* Copyright 2005 Joe Walker
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.directwebremoting.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.directwebremoting.extend.Handler;
/**
* A handler that deals with ETags and other nonsense to do with keeping a
* browsers cache in-sync with a web server.
* @author Joe Walker [joe at getahead dot ltd dot uk]
*/
public abstract class CachingHandler implements Handler
{
/* (non-Javadoc)
* @see org.directwebremoting.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
*/
public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException
{
long lastModified = getLastModifiedTime();
// Is the browser in sync with our latest?
if (isUpToDate(request, lastModified))
{
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
return;
}
// Is our cache up to date WRT the real resource?
CachedResource resource;
synchronized (scriptCache)
{
String url = request.getPathInfo();
resource = scriptCache.get(url);
if (resource == null || lastModified > resource.lastModifiedTime)
{
if (log.isDebugEnabled())
{
if (resource == null)
{
log.debug("Generating contents for " + url + ". It is not currently cached." );
}
else
{
log.debug("Generating contents for " + url + ". Resource modtime=" + lastModified + ". Cached modtime");
}
}
resource = new CachedResource();
resource.contents = generateCachableContent(request, response);
resource.lastModifiedTime = lastModified;
scriptCache.put(url, resource);
}
}
response.setContentType(mimeType);
response.setDateHeader(HttpConstants.HEADER_LAST_MODIFIED, lastModified);
response.setHeader(HttpConstants.HEADER_ETAG, "\"" + lastModified + '\"');
PrintWriter out = response.getWriter();
out.println(resource.contents);
}
/**
* Detect the last time, after which we are sure that the resource has not
* changed
* @return The last modification time
*/
protected abstract long getLastModifiedTime();
/**
* Create a String which can be cached and sent as a 302
* @param request The HTTP request data
* @param response Where we write the HTTP response data
* @return The string to output for this resource
* @throws IOException
*/
protected abstract String generateCachableContent(HttpServletRequest request, HttpServletResponse response) throws IOException;
/**
* Do we need to send the content for this file
* @param req The HTTP request
* @return true iff the ETags and If-Modified-Since headers say we have not changed
*/
protected boolean isUpToDate(HttpServletRequest req, long lastModified)
{
String etag = "\"" + lastModified + '\"';
if (ignoreLastModified)
{
return false;
}
long modifiedSince = -1;
try
{
// HACK: Websphere appears to get confused sometimes
modifiedSince = req.getDateHeader(HttpConstants.HEADER_IF_MODIFIED);
}
catch (RuntimeException ex)
{
// TODO: Check for "length" and re-parse
// Normally clients send If-Modified-Since in rfc-complaint form
// ("If-Modified-Since: Tue, 13 Mar 2007 13:11:09 GMT") some proxies
// or browsers add length to this header so it comes like
// ("If-Modified-Since: Tue, 13 Mar 2007 13:11:09 GMT; length=35946")
// Servlet spec says container can throw IllegalArgumentException
// if header value can not be parsed as http-date.
// We might want to check for "; length=" and then do our own parsing
// See: http://getahead.org/bugs/browse/DWR-20
// And: http://www-1.ibm.com/support/docview.wss?uid=swg1PK20062
}
if (modifiedSince != -1)
{
// Browsers are only accurate to the second
modifiedSince -= modifiedSince % 1000;
}
String givenEtag = req.getHeader(HttpConstants.HEADER_IF_NONE);
String pathInfo = req.getPathInfo();
// Deal with missing etags
if (givenEtag == null)
{
// There is no ETag, just go with If-Modified-Since
if (modifiedSince >= lastModified)
{
if (log.isDebugEnabled())
{
log.debug("Sending 304 for " + pathInfo + " If-Modified-Since=" + modifiedSince + ", Last-Modified=" + lastModified);
}
return true;
}
// There are no modified settings, carry on
return false;
}
// Deal with missing If-Modified-Since
if (modifiedSince == -1)
{
if (!etag.equals(givenEtag))
{
// There is an ETag, but no If-Modified-Since
if (log.isDebugEnabled())
{
log.debug("Sending 304 for " + pathInfo + ", If-Modified-Since=-1, Old ETag=" + givenEtag + ", New ETag=" + etag);
}
return true;
}
// There are no modified settings, carry on
return false;
}
// Do both values indicate that we are in-date?
if (etag.equals(givenEtag) && modifiedSince >= lastModified)
{
if (log.isDebugEnabled())
{
log.debug("Sending 304 for " + pathInfo + ", If-Modified-Since=" + modifiedSince + ", Last Modified=" + lastModified + ", Old ETag=" + givenEtag + ", New ETag=" + etag);
}
return true;
}
log.debug("Sending content for " + pathInfo + ", If-Modified-Since=" + modifiedSince + ", Last Modified=" + lastModified + ", Old ETag=" + givenEtag + ", New ETag=" + etag);
return false;
}
/**
* @param ignoreLastModified The ignoreLastModified to set.
*/
public void setIgnoreLastModified(boolean ignoreLastModified)
{
this.ignoreLastModified = ignoreLastModified;
}
/**
* The mime type to send the output under
* @param mimeType the mimeType to set
*/
public void setMimeType(String mimeType)
{
this.mimeType = mimeType;
}
/**
* @return the current mime type
*/
public String getMimeType()
{
return mimeType;
}
/**
* The mime type to send the output under
*/
private String mimeType;
/**
* We cache the script output for speed
*/
private final Map<String, CachedResource> scriptCache = new HashMap<String, CachedResource>();
/**
* I wish Java had tuples
*/
class CachedResource
{
protected String contents;
protected long lastModifiedTime;
}
/**
* Do we ignore all the Last-Modified/ETags blathering?
*/
private boolean ignoreLastModified = false;
/**
* The log stream
*/
private static final Log log = LogFactory.getLog(CachingHandler.class);
}