Friday, October 25, 2013

JSF 2: Returning a resource URL that is local to the web application

JSF 2 introduced a standard way of serving website assets - like JavaScript, CSS, image files, etc. It also introduced features which facilitate the insertion of these resources in to a JSF page.

Consider the following web application code base layout:



This application, as you can see, uses the Bootstrap framework, which requires JQuery as a prerequisite. With the new resource loading solution, you can include the above files in your XHTML template by adding the following declarations:


   <h:head>
      <h:outputStylesheet library="bootstrap" name="css/bootstrap.css"/>
      <h:outputStylesheet library="bootstrap"
                          name="css/bootstrap-theme.css"/>
      <h:outputScript library="jquery" name="js/jquery.min.js"/>
      <h:outputScript library="bootstrap" name="js/bootstrap.min.js"/>
    </h:head>


Notice in the above declarations, there is no version number provided for the JQuery script, and in the code base, we are using '_'s instead of '.'s to separate the version numbers. In JSF 2, the ResourceHandler takes care of identifying the latest version of your Resource and renders the appropriate file. This is an extremely useful feature, where we can update our resources and JSF will ensure that the latest version is the artefact that is rendered.

Everything works fine, with the exception of on small glitch! Relative references inside your CSS file do not work! This has to do with the way the resources are rendered. If you are using JSF with the suffix mapping (the typical scenario where you say that all .xhtml file are to be processed by JSF), then the above declarations would produce the following output:


    /javax.faces.resource/css/bootstrap.min.css.xhtml?ln=bootstrap
    /javax.faces.resource/js/jquery.js.xhtml?ln=jquery&v=1_10_2

, so if the bootstrap.min.css file had a relative reference to the fonts file, via the following declaration:

    url('../fonts/glyphicons-halflings-regular.eot');

The URL that gets constructed does not make a lot of sense

    /javax.faces.resource/fonts/glyphicons-halflings-regular.eot

As you can see, the above URL does not exist, and since there is no .xhtml file at the end of the URL the Faces servlet doesn't handle it either.

Now there are 3 ways of handling this issue:

  1. Don't use the JSF resource loading mechanics. Instead include the web resources by constructing the URL directly

    <link rel="stylesheet" type="text/css"
          href="#{request.contextPath}/resources/bootstrap/css/bootstrap.min.css"/>
     
  2. Modify the CSS file and update all the relative URLs to be JSF aware

    url('../fonts/glyphicons-halflings-regular.eot.xhtml?ln=bootstrap');
  3. Or, you can write a custom ResourceHandler that will:
    • Allow you to use external web assets without making any changes to them.
    • Leverage the JSF resource loading mechanics and all the goodness that comes along with it.
    • Generate a URL that looks like option 1.

The Custom ResourceHandler


One really good feature about JSF is that you can replace / augment pretty much any part of the framework. In order to achieve our use case, we will define a new ResourceHandler and an associated Resource type.



The AppResourceHandler extends the ResourceHandlerWrapper, and monitors only those method calls that create a resource object.

For every resource object that gets created, the URL of the resource is analyzed to determine if that resource is local to the webapp or part of some archive (jar / zip / etc.) file in the class path.

AppResourceHandler.java
...

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

/**
 * If the given resource object can be rendered locally, then do so by
 * returning a wrapped object, otherwise return the input as is.
 */
private Resource getWrappedResource(Resource resource) {
    WebAppResource webAppResource = null;
    ExternalContext context = FacesContext.getCurrentInstance()
                                          .getExternalContext();
    // Get hold of the webapp resources directory name
    if (resourcesRoot == null) {
        String resourcesRoot = context.getInitParameter(
                ResourceHandler.WEBAPP_RESOURCES_DIRECTORY_PARAM_NAME);
        if (resourcesRoot == null) {
            resourcesRoot = "/resources";
        }
    }

    if (resource != null) {
        URL baseURL = resource.getURL();
        if (baseURL != null) {
            String extForm = baseURL.toExternalForm();
            int idx = extForm.indexOf(resourcesRoot);
            if (idx != -1) {
                try {
                    extForm = extForm.substring(idx);
                    URL resourceURL = context.getResource(extForm);
                    if (resourceURL != null) {
                        webAppResource = new WebAppResource(extForm,
                                                            resource);
                    }
                } catch (MalformedURLException e) {}
            }
        }
    }
    return webAppResource != null ? webAppResource : resource;
}

And in the WebAppResource class, we just return the webapp local URL instead of a Faces one.

WebAppResource.java
...
private Resource wrapped;
private String path;

public WebAppResource(String path, Resource wrapped)
{
    this.wrapped = wrapped;
    this.path = path;
}

@Override
public String getRequestPath()
{
    FacesContext context = FacesContext.getCurrentInstance();
    return context.getApplication().getViewHandler()
                  .getResourceURL(context, path);
}

Finally, we add the custom resource handler's entry in the faces-config.xml file.

<faces-config ...>
    <application>
        <resource-handler>
            some.package.AppResourceHandler
        </resource-handler>
    </application>
</faces-config>


And we are done. Run your code and see the difference!

Hope this was useful to you.

No comments: