Monday, April 25, 2011

Enabling caching in REST

Ideally the resources like images, flash files etc should be cached on the client browser or proxy server to reduce server load and reduce bandwidth consumption. In this post i will explain on how one can enable caching of these resources if they are exposed as restful resources using JERSEY Restful framework.

To enable caching of resources by proxy server or by client you need to keep a last modified date column in your database table which is updated if you upload or modify the content. The solution that i am discussing over here is as follows :-

1. If it is a initial request for the resource we will fetch the information from a data source and then build a response setting the last modified property on ResponseBuilder  along with the max age value for CacheControl header.

2. The cached content will be forced to expire after 5 minutes after which if a request for a resource is sent to the proxy server it will be forwarded to the container, when the request is received by the container we only need to compare the modified time of the resource with the value in the request header,Only if the resource has indeed been changed will we build a new response(setting the aforementioned header values) otherwise we will tell the client to reuse the cached content.

The above solution will ensure that a client will cache the response for 5 minutes and only if the content has been changed during these 5 minutes will it be re-fetched from the data source. This solution will drastically reduce the load on server and the client side.

The necessary code to accomplish this is mentioned below:-

@Path("/") 
public class FileResource {
/**
sets a 5 minutes expiry time

**/
public ResponseBuilder setExpiry(ResponseBuilder rbuilder){
int maxAge;
GregorianCalendar now=new GregorianCalendar();
//trims the milliseconds
maxAge=(int) ((now.getTimeInMillis()/1000L)+(5*60));
CacheControl cc=new CacheControl();
cc.setMaxAge(maxAge);
rbuilder.cacheControl(cc);
return rbuilder;

}


/**
* @param filename passed in the get request
* @returns builds and returns a response
*
*
**/
@GET
@Path("/files/{filename}")
@Produces(MediaType.WILDCARD)
public Response returnFileAsStream(@Context Request request,@PathParam("filename") String fileName){
FileStoreDAO ftDAO=FileStoreDAO.getInstance();
FileUtility futil=FileUtility.getInstance();
Date lastModified=ftDAO.getLastModifiedTime(fileName);
// re-validate whether file has changed since last modified time
ResponseBuilder respBuilder=request.evaluatePreconditions(lastModified);
if(respBuilder!=null){
//sets 5 minute cache expiry time
return setExpiry(respBuilder).build();

}
else{
//reads the file and sets its value into a DTO
FileDTO fdto=ftDAO.readFile(fileName);
if(fdto!=null){
String contentType=fdto.getMime();
InputStream is=fdto.getFileData();
respBuilder=Response.ok(is,contentType);
//sets the last modified header value
respBuilder.lastModified(lastModified);
return setExpiry(respBuilder).build();

}
}
}

The above code creates the response using the response builder, sets the max age value in cache control header and the value for the last modified header for the response.

The code from the DAO layer is shown below that shows a important point about HTTP proxy, the proxy does not handle milliseconds so we must set the value for milliseconds to 0 when comparing the last modified date value otherwise the comparison would never succeed.

/**
* @param fileName the filename
* @return the time truncated to seconds
*/
public java.util.Date getLastModifiedTime(String fileName){
//get prepared statement and execute a query
pstmt=conn.getPreparedStatement("select last_mod_date from editor_file_store where file_name= ?");
pstmt.setString(1,fileName);
//populate result set
rs=pstmt.executeQuery();

if(rs.next()){
sqlDate= rs.getDate(1);
}
//set this value on a calendar variable and set the millisecond to 0
cal.setTime(sqlDate);
cal.set(Calendar.MILLISECOND, 0);


return cal.getTime();
catch (SQLException e) {
logger.log("Exception occured"+ e.getMessage());
}
finally{
// close result set and connection
try {
if(rs!=null){
rs.close();
}
if (pstmt != null) {
pstmt.close();

}
conn.closeConnection();
}
}
catch (SQLException e1) {
logger.log( "An error occured due to "+e1.getMessage());
}
}
return cal.getTime();

}
After changing the above code you can try using firebug for firefox and observe the values for the request and response headers in the net tab. You will notice the values for cache control and last-modified headers and the fact that content needs to be resent only in case it has been modified.

Hope this tutorial was helpful, kindly report any error that you may find it is duly appreciated.