Sunday, February 20, 2011

Invalidate browser cache with custom JSF resource handler

Browser caches static resources like CSS, JavaScript files, images if they don't have proper expire time in the response header. An usual task is to find a solution which allows refetching of resources with each software release. We can modify all resource URLs by appending "&rv=1234" or "?rv=1234" to the end. rv is a current software revision. Such modified URL leads to the refetching of resources. The current revision can be acquired during project build and stored somewhere, e.g. in a text file, in order to make it available in the web application. Maven has e.g. a special plugin for this task called buildnumber-maven-plugin. This plugin allows to get the current revision from a version control system.

We will write a custom JSF resource handler CustomResourceHandler which will accomplish the URL modifying. The resource handler can be registered in faces-config.xml as follows:
<application>
    <resource-handler>packagename.CustomResourceHandler</resource-handler>
</application>
Writing of any resource handler is straightforward. We need to extend javax.faces.application.ResourceHandlerWrapper
public class CustomResourceHandler extends javax.faces.application.ResourceHandlerWrapper
{
    private ResourceHandler wrapped;

    public CustomResourceHandler(ResourceHandler wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public ResourceHandler getWrapped() {
        return this.wrapped;
    }

    @Override
    public Resource createResource(String resourceName, String libraryName) {
        Resource resource = super.createResource(resourceName, libraryName);

        // here a check of library name could be necessary, etc.
        ...
        return new CustomResource(resource);
    }
}
We need now a class CustomResource. CustomResource should extend javax.faces.application.ResourceWrapper and delegate all calls to this wrapper class except one call getRequestPath(). Method getRequestPath() needs to be overwritten.
public class CustomResource extends javax.faces.application.ResourceWrapper
{
    private javax.faces.application.Resource resource;

    public CustomResource(Resource resource) {
        this.resource = resource;
    }

    @Override
    public Resource getWrapped() {
        return this.resource;
    }

    @Override
    public String getRequestPath() {
        String requestPath = resource.getRequestPath();
                 
        // get current revision
        String revision = ...

        if(requestPath.contains("?"))
            requestPath = requestPath + "&rv=" + revision;
        else
            requestPath = requestPath + "?rv=" + revision;

        return requestPath;
    }

    @Override
    public String getContentType() {
        return getWrapped().getContentType();
    }

    ...
}
That's all. For more control in Mojarra JSF implementation there are two handy config parameters for web.xml.

com.sun.faces.defaultResourceMaxAge set expire time (in milliseconds) into response header and is responsible for browser-caching. Default value in Production ProjectStage is ca. 10 min.
com.sun.faces. resourceUpdateCheckPeriod gives frequency in minutes to check for changes to webapp artifacts that contain resources.

8 comments:

  1. An interesting situation arises with a custom resource handler and richfaces when using the packed/compressed resources available in RF4.1. In the place where it says // here a check of library name could be necessary, etc. you need to check if the libraryName is "org.richfaces" and just "return resource;".

    If you use the jquery distribution from within the richfaces libraries then just using h:outputScript name="jquery.js" will cause the richfaces packed.js to load, and it doesn't like having a query string appended to it. So you also need to check in this same place if the resourceName is "jquery.js" and "return resource;" - not return new CustomResource(resource);.

    Regards,
    healeyb

    ReplyDelete
  2. Nice solution! Exactly what I was looking for. Thanks!

    ReplyDelete
  3. Unfortunately the tags were truncated. The tags I tried to mention are:
    <h:outputStylesheet />, <h:outputScript /> and <h:graphicImage />

    ReplyDelete
  4. I'm using a code like this. But raise NullPointerException when I try to open a facelets page that contains a composite component.

    So, in my project, I had to undo the changes.

    ReplyDelete
  5. Great post, RE the NullPointerException raised for composite components, adding

    @Override
    public String getLibraryName() {
    return resource.getLibraryName();
    }

    @Override
    public String getResourceName() {
    return resource.getResourceName();
    }

    will fix this in Majorra. It looks like an over-site in the ResourceWrapper implementation that it does not provide these.

    ReplyDelete
  6. You made some decent points there. I looked on the internet for the issue and found most individuals will go along with with your website.

    Bespoke Software Development.

    ReplyDelete
  7. Nice solution! Big up!

    ReplyDelete
  8. Thanks, it worked perfectly for me

    ReplyDelete

Note: Only a member of this blog may post a comment.