Wednesday, February 28, 2007

Tomcat Cache Control Customization

In the last post Customize Tomcat's Static File Serving we discussed how to customize file serving in our application.

If we allow Tomcat DefaultServlet to serve the static resource, it sends back ETag header which is used for cache control on the client browser. The problem with this is client (browser) will send request to check if the file is modified each time we refresh the page. This will add overhead to our application as well as server.

We might want to completely avoid client browser (if cache is enabled) from requesting a resource until sometime, in such case right cache control headers should be sent back. Here is our new CustomFileServlet which add custom cache headers if the resource served is javascript or css file and such resource will asked to cache for a day.

package com.example.servlet;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.servlets.DefaultServlet;

public class CustomFileServlet extends DefaultServlet {
private static final long serialVersionUID = 1L;
public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
if (processRequest(request, response)) {
super.doPost(request, response);
}
}
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
if (processRequest(request, response)) {
super.doGet(request, response);
}
}
public boolean processRequest(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
String requestURI = request.getRequestURI();
String contextPath = request.getContextPath();

String fileURI = "";
if (requestURI.indexOf(contextPath) != -1) {
// Request URI is CONTEXTPATH + RESOURCE URI
fileURI = requestURI.substring(
requestURI.indexOf(contextPath)
+ contextPath.length());
}

File file = new File(request.getSession()
.getServletContext().getRealPath(fileURI));
String lowerfile = file.getAbsolutePath().toLowerCase();

boolean isBasePath = "/".equalsIgnoreCase(fileURI);
boolean isJSFile = lowerfile.endsWith(".js");
boolean isCSSFile = lowerfile.endsWith(".css");
boolean customServing = (!isBasePath) && (isJSFile || isCSSFile);

if(customServing) {
String mimetype = request.getSession()
.getServletContext().getMimeType(fileURI);
response.setContentType(mimetype);

// Required Cache Control Headers
String maxage = "86400"; // One day in Seconds
response.setHeader("Cache-Control", "max-age="+ maxage);
long relExpiresInMillis = System.currentTimeMillis() + (1000 * Long.parseLong(maxage));
response.setHeader("Expires", getGMTTimeString(relExpiresInMillis));
response.setHeader("Last-Modified", getGMTTimeString(file.lastModified()));

// Serve the file content
FileInputStream fis = new FileInputStream(file);
OutputStream ostream = response.getOutputStream();
streamIO(fis, ostream);
fis.close();
ostream.flush();
ostream.close();

return false; // We have taken care of file serving.
}
return true; // Let DefaultServlet handle file serving
}
public static String getGMTTimeString(long milliSeconds) {
SimpleDateFormat sdf = new SimpleDateFormat("E, d MMM yyyy HH:mm:ss 'GMT'");
return sdf.format(new Date(milliSeconds));
}
public static void streamIO(InputStream is, OutputStream os)
throws IOException {
byte[] bytes = new byte[2048];
int readlen = -1;
while ((readlen = is.read(bytes)) != -1) {
os.write(bytes, 0, readlen);
os.flush(); // Let us flush after bluk write
}
}


Customize Tomcat's Static File Serving

Tomcat maps the static file request to DefaultServlet class (org.apache.catalina.servlets.DefaultServlet in Tomcat/server/lib/servlets-default.jar).

If you want to customize file serving the following steps need to be followed.
1. Mapping in web.xml for you application
<servlet>
<servlet-name>CustomFileServlet</servlet-name>
<servlet-class>com.example.servlet.CustomFileServlet</servlet-class>
<init-param>
<param-name>listing</param-name>
<param-value>true</param-value>
<!-- allow browsing directory inside web application -->
</servlet>
<servlet-mapping>
<servlet-name>CustomeFileServlet</servlet-name>
<url-pattern>/</url-pattern>
<!-- Maps all request except the ones which are mapped separately -->
</servlet-mapping>

2. Enable privilege for the application (since server library class is used).
Here is an example context file I created for such a application (Tomcat/conf/Catalina/localhost/customapp.xml)
<?xml version="1.0"?>
<context path="/customapp" docbase="/home/prasad/CustomApp/web" privileged="true" reloadable="true"></context>

3. Write a CustomFileServlet class.
package com.example.servlet;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.servlets.DefaultServlet;
public class CustomFileServlet extends DefaultServlet {
private static final long serialVersionUID = 1L;
public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
if (processRequest(request, response)) {
super.doPost(request, response);
}
}

public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
if (processRequest(request, response)) {
super.doGet(request, response);
}
}

public boolean processRequest(HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException {
String requestURI = request.getRequestURI();
String contextPath = request.getContextPath();

String fileURI = "";
if (requestURI.indexOf(contextPath) != -1) {
// Request URI is CONTEXTPATH + RESOURCE URI
fileURI = requestURI.substring(
requestURI.indexOf(contextPath) +
contextPath.length());
}

boolean checkPassed = false;
// DO SOME CHECKS BEFORE SERVING FILE...

return checkPassed;
}


This page is powered by Blogger. Isn't yours?