How to Implement "CSS Versioning" (To Solve Cache Issues) Using Jsf 2 H:Outputstylesheet

How can I implement CSS versioning (to solve cache issues) using JSF 2 h:outputStylesheet?

Facing the same challenge, I ended up extending javax.faces.application.ResourceHandlerWrapper and javax.faces.application.ResourceWrapper to append "&v=x.y.z" to the result of ResourceWrapper#getRequestString().

I saw this kind of solution implemented by Primefaces and Openfaces.
Just take a look at the source of

org.primefaces.application.PrimeResourceHandler#createResource(String resourceName, String libraryName)

and

org.primefaces.application.PrimeResource#getRequestPath()

Available here.

Don't forget to add your implementation to faces-config.xml:

<application>
<resource-handler>your.package.YourResourceHandlerWrapper</resource-handler>
</application>

How to use JSF versioning for resources in jar

That's unfortunately not possible. Library versioning is not supported for resources in JAR.

You've basically 2 options:

  1. Do it the easy and ugly way, include server's startup time as query string. Given that you're using OmniFaces, you could use its builtin #{startup} managed bean referring a java.util.Date instance in application scope:

    <h:outputStylesheet ... name="some.css?#{startup.time}" />
    <h:outputScript ... name="some.js?#{startup.time}" />

    Or perhaps you've the version already as some application variable.

    <h:outputStylesheet ... name="some.css?v=#{app.version}" />
    <h:outputScript ... name="some.js?v=#{app.version}" />

    Update: Notwithstanding, this doesn't work for <h:outputStylesheet>. See also: https://github.com/javaserverfaces/mojarra/issues/3945 or https://github.com/javaee/javaserverfaces-spec/issues/1395

    It works for <h:outputScript> though, which had a very simliar bug report which was implemented pretty soon https://github.com/javaserverfaces/mojarra/issues/1216

  2. Do the same as PrimeFaces, create a custom ResourceHandler.

    public class MyVersionResourceHandler extends ResourceHandlerWrapper {

    private ResourceHandler wrapped;

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

    @Override
    public Resource createResource(String resourceName) {
    return createResource(resourceName, null, null);
    }

    @Override
    public Resource createResource(String resourceName, String libraryName) {
    return createResource(resourceName, libraryName, null);
    }

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

    if (resource == null) {
    return null;
    }

    return new ResourceWrapper() {

    @Override
    public String getRequestPath() {
    return super.getRequestPath() + "&v=1.0";
    }

    @Override // Necessary because this is missing in ResourceWrapper (will be fixed in JSF 2.2).
    public String getResourceName() {
    return resource.getResourceName();
    }

    @Override // Necessary because this is missing in ResourceWrapper (will be fixed in JSF 2.2).
    public String getLibraryName() {
    return resource.getLibraryName();
    }

    @Override // Necessary because this is missing in ResourceWrapper (will be fixed in JSF 2.2).
    public String getContentType() {
    return resource.getContentType();
    }

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

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

    }

    Or if you happen to already use OmniFaces, it could be done simpler:

    public class YourVersionResourceHandler extends DefaultResourceHandler {

    public YourVersionResourceHandler(ResourceHandler wrapped) {
    super(wrapped);
    }

    @Override
    public Resource decorateResource(Resource resource) {
    if (resource == null || !"mylib".equals(resource.getLibraryName())) {
    return resource;
    }

    return new RemappedResource(resource, resource.getRequestPath() + "&v=1.0");
    }

    }

    Either way, to get it to run, register it as <resource-handler> in /META-INF/faces-config.xml of the JAR.

    <application>
    <resource-handler>com.example.MyVersionResourceHandler</resource-handler>
    </application>

What does '?' do in a Css link?

That is there to add some uniqueness to the filename, so that when they change the CSS file, they can change the extra bit to be totally sure that every client will reload the CSS rather than use a cached version.

The webserver will ignore the parameter and serve /Content/all.min.css normally

Note: While it's possible the CSS is dynamically generated, this is a common idiom for ensuring a reload, and given the parameter is a date, it seems quite likely.


Edit: Podcast 38 mentioned this...

We’ve been using the Expires or
Cache-Control Header since we
launched. This saves the browser
round-trips when getting infrequently
changing items, such as images,
javascript, or css. The downside is
that, when you do actually change
these files, you have to remember to
change the filenames. A part of our
build process now “tags” these files
with a version number so we no longer
have to remember to do this manually.

How to clean cache after a new deployment?

I would suggest to use the build in JSF resource library mechanism, which supports versioning of resources. Since the clients caches will load a given resource when its' path changes. This way you can change the version number of your resoures and urge the client to reload them, without having to deal with any cache ageing strategies.

There are several good write-ups about this topic here on SO

  • What is the JSF resource library for and how should it be used?
  • JSF resource versioning
  • JSF2 Static resource caching

@ResourceDependency with string query

Answering the question based on @Tarik's comment.

I added this to my faces-config.xml:

<application>
<resource-handler>my.package.MyResourceHandlerImplementation</resource-handler>
</application>

MyResourceHandlerImplementation looks like this:

import com.sun.faces.application.resource.ResourceHandlerImpl;

public class MyResourceHandlerImplementation extends ResourceHandlerImpl {

@Override
public String getRendererTypeForResourceName(String resourceName) {
if (hasQueryString(resourceName)) {
resourceName = removeQueryString(resourceName);
}
return super.getRendererTypeForResourceName(resourceName);
}

private boolean hasQueryString(String resourceName) {
return resourceName.indexOf('?') != -1;
}

private String removeQueryString(String resourceName) {
return resourceName.substring(0, resourceName.indexOf('?'));
}

}

And this is how you make @ResourceDependency support query strings (thus supporting cache busting).

Resources first checked against server not retrieved from cache right away

I figured out that the server was responding with Cache-Control: no-cache and Pragma: no-cache because the resources were accessed on a secure page that was defined by a GlassFish Security Realm (j_security_check).

To solve this I implementend the solution here: Static resources are not cached referenced from glassfish secure page

I added <property name="securePagesWithPragma" value="false" /> as a property from glassfish-web-app in the glassfish-web.xml. Now only the Cache-Control header is set with the value, private which is fine. Private means that only the end user can cache it and not inbetween proxies.

What advantages do JSF's h:outputScript/ h:outputStylesheet have over plain HTML script/style elements

The JSF builtin resource handling has at least the following advantages:

  • Resource versioning
  • Configurable cache control
  • Packaging in JAR
  • Programmatic (component based) manipulation
  • Automatic prepending of right context path
  • EL support in CSS files
  • i18n support (different resource files based on user's locale)
  • Automatic HTTP/2 push (JSF 2.3+ on HTTPS only)

See also:

  • How to reference CSS / JS / image resource in Facelets template?
  • What is the JSF resource library for and how should it be used?


Related Topics



Leave a reply



Submit