cachingjsfprimefaces

How to Add Dynamic Versioning to PrimeFaces Theme URL for Cache Busting?


I am using PrimeFaces 12 in my JSF application and have a custom theme. The theme is included automatically by PrimeFaces, and its resource URL looks like this:

<link type="text/css" rel="stylesheet" href="/GuiApp/jakarta.faces.resource/theme.css.xhtml?ln=primefaces-custom&amp;v=12.0.0"> 

The problem is that when I update the theme's CSS, users often see the old cached version because the browser does not fetch the updated file. To solve this, I want to add a dynamic version number to the theme URL, such as v=12.0.1, so that the browser always fetches the latest version.

What I Have Tried:

1. Added a theme.version context parameter in web.xml:

    <context-param>
        <param-name>theme.version</param-name>
        <param-value>12.0.1</param-value>
    </context-param>

2. Created a custom ResourceHandler to append the version to the URL:

@Override
public Resource createResource(String resourceName, String libraryName) {
    if ("primefaces-custom".equals(libraryName) && "theme.css".equals(resourceName)) {
        Resource resource = super.createResource(resourceName, libraryName);
        String version = FacesContext.getCurrentInstance()
            .getExternalContext()
            .getInitParameter("theme.version");
        return new VersionedResource(resource, version);
    }
    return super.createResource(resourceName, libraryName);
}

3. Wrapped the resource to modify the URL:

@Override
public String getRequestPath() {
    String path = wrapped.getRequestPath();
    if (path.contains("?")) {
        path = path.replaceAll("[&?]v=[^&]*", ""); // Remove existing version
    }
    return path + (path.contains("?") ? "&" : "?") + "v=" + version;
}

Problem:

When I implemented this, the theme URL was updated, but it included duplicate version parameters like this:

<link type="text/css" rel="stylesheet" href="/GuiApp/jakarta.faces.resource/theme.css.xhtml?ln=primefaces-custom&amp;v=12.0.0&amp;v=12.0.1">

After deploying, my application broke, and some resources stopped loading.

What I Need:

  1. How can I properly add a dynamic version (v) to the theme URL without duplicating parameters?
  2. Is overriding the ResourceHandler the correct approach for PrimeFaces theme versioning, or is there a better solution?

Solution

  • Here is my Step-by-Step Solution:

    Add a context-param in web.xml to define the theme version:

    In your web.xml, define a context-param to specify the theme version you want to use.

    <context-param>
        <param-name>theme.version</param-name>
        <param-value>12.0.1</param-value> <!-- Update this version when you release a new one -->
    </context-param>
    

    Create a Custom ViewHandler to dynamically set the version in the theme URL:

    Next, extend the default ViewHandler and override the createResource method. This method will dynamically set the version of the theme's CSS URL by fetching the version from the web.xml.

    public class CustomResourceHandler extends ViewHandler {
        private ViewHandler parent;
    
        public CustomResourceHandler(ViewHandler parent) {
            this.parent = parent;
        }
    
        // Method to get the theme version from web.xml
        private String getThemeVersion(FacesContext context) {
            String version = context.getExternalContext().getInitParameter("theme.version");
            return (version != null) ? version : "12.0.0"; // Default version if not set
        }
    
        @Override
        public Resource createResource(String resourceName, String libraryName) {
            if ("primefaces-custom".equals(libraryName) && "theme.css".equals(resourceName)) {
                Resource resource = super.createResource(resourceName, libraryName);
                if (resource != null) {
                    FacesContext context = FacesContext.getCurrentInstance();
                    String themeVersion = getThemeVersion(context);
    
                    // Dynamically update the resource path with the version
                    String newPath = resource.getRequestPath().replaceAll("&v=[0-9.]+", "&v=" + themeVersion);
                    return new ResourceWrapper(resource) {
                        @Override
                        public String getRequestPath() {
                            return newPath;
                        }
                    };
                }
            }
            return super.createResource(resourceName, libraryName);
        }
    }
    

    Create VersionedResource to Modify the URL

    import jakarta.faces.application.Resource;
    import jakarta.faces.context.FacesContext;
    
    public class VersionedResource extends Resource {
    
        private final Resource wrapped;
        private final String version;
    
        public VersionedResource(Resource wrapped, String version) {
            this.wrapped = wrapped;
            this.version = version;
        }
    
        @Override
        public String getRequestPath() {
            String path = wrapped.getRequestPath();
    
            if (path.contains("?")) {
                path = path.replaceAll("[&?]v=[^&]*", "");
            }
    
            if (path.contains("?")) {
                return path + "&v=" + version;
            } else {
                return path + "?v=" + version;
            }
        }
    
        @Override
        public InputStream getInputStream() throws IOException {
            return wrapped.getInputStream();
        }
    
        @Override
        public Map<String, String> getResponseHeaders() {
            return wrapped.getResponseHeaders();
        }
    
        @Override
        public String getContentType() {
            return wrapped.getContentType();
        }
    
        @Override
        public String getLibraryName() {
            return wrapped.getLibraryName();
        }
    
        @Override
        public String getResourceName() {
            return wrapped.getResourceName();
        }
    
        @Override
        public URL getURL()
        {
            // TODO Auto-generated method stub
            return null;
        }
    
        @Override
        public boolean userAgentNeedsUpdate(FacesContext context)
        {
            // TODO Auto-generated method stub
            return false;
        }
    }
    

    Register the custom ViewHandler in faces-config.xml:

    <application>
        <view-handler>com.yourpackage.CustomResourceHandler</view-handler>
    </application>