Tuesday, July 31, 2012

Skinning in JSF


Why Skinning?

CSS is the tool for webdevelopers (or more likely designers) to customize sites without having to alter code. This allows for visual variability without the need to compile/test/deploy/... things. It also allows user-specific theming where the user can decide which theme looks best.
However there is one thing notably lacking in css: variables. They may be introduced in CSS3 but it will be a ways before they are actually picked up by all major browsers. Until then we can look at other frameworks to see how they achieve skinning in JSF, most notably Richfaces. This document is written using richfaces as a guideline but does not actually require any richfaces components to be present so it can be used with any JSF framework.

Richfaces skinning

Richfaces supports skinning by using a simple properties file to represent the variables. The properties file must:
  • be located in META-INF/skins
  • have the extension "skin.properties"
For example you might create a skin "META-INF/skins/test.skin.properties". To tell richfaces to use this skin, you have to configure a context parameter in the web.xml file:
<context-param>
      <param-name>org.richfaces.skin</param-name>
      <param-value>test</param-value>
</context-param>
You can take an existing skin file to see which variables exist or you can browse the component pages to see which variables they reference. Additionally richfaces will by default (you can turn this off) style regular jsf components as well in order to create a single theme for the entire application.

Using the variables yourself

Of course you don't only want to skin existing richfaces components, you may want to use the variables in the css of your own components/pages. In order to do this, you need to create a file with an extension ".ecss" instead of ".css". This will be automatically picked up by richfaces and the variables inside it will be replaced with the properties available in the skin file. Each variable must be written like this:
'#{richSkin.myParam}'
The quotes are mandatory. There are however a number of caveats.

Library lookups are wonky

Richfaces will translate your ecss outputStylesheet to something like this:
<link type = "text/css" rel = "stylesheet"
      href = "/app/rfRes/myStyleSheet.ecss.xhtml?db=eAHb-CCOEQAGJQHx" />
Which is fine unless you add the style sheet to a library, then it will generate this (note the escaped ampersand):
<link type = "text/css" rel = "stylesheet"
      href = "/app/rfRes/myStyleSheet.ecss.xhtml?db=eAHb-CCOEQAGJQHx&amp;ln=style" />
I'm not entirely sure if the problem lies with richfaces (I don't really see an obvious bug in the code with regards to URI encoding) or with jsf in general but either way it is a bit off. This may work depending on the browser, not sure how well supported this is though. Anyway, you can easily sidestep this by not putting it in a resource library and simply adding stuff on the root.

Eclipse does not like ecss files

Eclipse does not always like the variable format ecss uses. Depending on your level of bad luck you either get badly highlighted css or continuous parsing errors resulting in error popups.

Custom css properties are dropped

It is a long standing tradition to introduce new css features in browsers with a prefix for said browsers. For example take this bit of css which includes a chrome/firefox gradient and a firefox/general box-sizing declaration:
.menu .main a {
      background: -webkit-gradient(linear, left top, left bottom, from('#{richSkin.menuGradientLight}'), to('#{richSkin.menuGradientDark}'));
      background: -moz-linear-gradient(top, '#{richSkin.menuGradientLight}', '#{richSkin.menuGradientDark}');
      padding: 0px 10px;
      padding-top: 8px;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
}
When I browsed the resulting compiled ecss, I got this:
*.menu *.main a {
      background: webkit-gradient(linear,lefttop,leftbottom,from(rgb(64,64,64)),to(rgb(43,43,43)));
      background: webkit-gradient(linear,lefttop,leftbottom,from(rgb(64,64,64)),to(rgb(43,43,43)));
      padding: 0px 10px;
      padding-top: 8px;
}
As you can see, it dropped the mozilla gradient and both the mozilla and default box-sizing properties. This is likely because richfaces actually interprets the css instead of merely replacing the variables. The colors were also defined as hexadecimal and converted to their rgb equivalent. Apart from validation, I'm not entirely sure why richfaces bothers parsing the css.

Custom variable styling

To work around the issues mentioned above, I wrote my own stylesheet compiler. It requires a few things to function:
  • stylesheets must use the extension ".compiled.css" instead of ".css", so for example "mystyle.compiled.css" will be picked up by the stylesheet compiler
  • it currently uses the richfaces web.xml property mentioned above to determine which skin file you want and also scans for the "META-INF/skins/<name>.skin.properties" file, in that respect it is compatible with richfaces
The variable format is slightly more lightweight, it uses "$", the css fragment in the above would become:
.menu .main a {
      background: -webkit-gradient(linear, left top, left bottom, from($menuGradientLight), to($menuGradientDark));
      background: -moz-linear-gradient(top, $menuGradientLight, $menuGradientDark);
      padding: 0px 10px;
      padding-top: 8px;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
}
The compiler does not actually parse the stylesheet, it simply replaces all the variables it finds in the skin properties file.

Bits and pieces

The stylesheet compiler is a standalone jar file which can be plugged into any war file. It contains the following things.

CSSResourceHandler.java

First off you need to provide a custom implementation of the jsf resource handler. Based on this tutorial and the richfaces implementation it looks like this:
public class CSSResourceHandler extends javax.faces.application.ResourceHandlerWrapper {
      private Logger logger = LoggerFactory.getLogger(getClass());
      
      private Properties skinProperties;
      
      private ResourceHandler wrapped;
      
      public CSSResourceHandler(ResourceHandler wrapped) {
            logger.debug("Creating custom resource handler with parent {}", wrapped);
            this.wrapped = wrapped;
      }
      
      @Override
      public ResourceHandler getWrapped() {
            return wrapped;
      }
      @Override
      public Resource createResource(String resourceName, String library, String contentType) {
            logger.trace("createResource(" + resourceName + ", " + library + ", " + contentType + ")");
            // get the resource in the conventional way
            Resource resource = super.createResource(resourceName, library, contentType);
            // if the resource is a ".compiled.css" file, compile it
            if (resourceName.endsWith(".compiled.css"))
                  return new CompiledCSS(resource, getSkinProperties());
            else
                  return resource;
      }
      @Override
      public Resource createResource(String resourceName, String library) {
            return createResource(resourceName, library, null);
      }
      @Override
      public Resource createResource(String resourceName) {
            return createResource(resourceName, null, null);
      }
      
      @Override
public String getRendererTypeForResourceName(String resourceName) {
            if (resourceName.endsWith(".compiled.css"))
                  return "javax.faces.resource.Stylesheet";
            else
                  return super.getRendererTypeForResourceName(resourceName);
      }
      
      private Properties getSkinProperties() {
            if (skinProperties == null) {
                  // we need to figure out which skin is configured
                  FacesContext context = FacesContext.getCurrentInstance();
                  // get the richfaces skin parameter
                  String skin = context.getExternalContext().getInitParameter("org.richfaces.skin");
                  // the default skin
                  if (skin == null)
                        skin = "DEFAULT";
                  // in richfaces, the context class loader is used, not the faces context method of resource discovery
                  // note that the path must follow the below convention as per richfaces documentation & implementation
                  InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream("META-INF/skins/" + skin + ".skin.properties");
                  if (input != null) {
                        logger.debug("Loading skin " + skin);
                        skinProperties = new Properties();
                        try {
                              try {
                                    skinProperties.load(input);
                              }
                              finally {
                                    input.close();
                              }
                        }
                        catch (IOException e) {
                              throw new RuntimeException(e);
                        }
                  }
                  else
                        logger.error("Could not find skin " + skin);
            }
            return skinProperties;
      }
}
As you can see, it simply delegates most resource calls to the wrapper that it was initiated with. Only when the extension ".compiled.css" is found does it kick in. If the web.xml context parameter is not set, it will fall back to the richfaces default.

CompiledCSS.java

The compiled css class extends resource and much like the handler, delegates nearly all calls. It intercepts the request for content though and performs a replace on the original content before sending it back:
public class CompiledCSS extends Resource {
      private Resource original;
      private String cached;
      
      private Properties skinProperties;
      
      public CompiledCSS(Resource original, Properties skinProperties) {
            this.original = original;
            this.skinProperties = skinProperties;
      }
      
      /**
       * This is where we actually convert the css
       */
      @Override
      public InputStream getInputStream() throws IOException {
            if (cached == null) {
                  // copy to string
                  ByteArrayOutputStream output = new ByteArrayOutputStream();
                  byte [] buffer = new byte[102400];
                  int read;
                  InputStream source = original.getInputStream();
                  try {
                        while ((read = source.read(buffer)) != -1)
                              output.write(buffer, 0, read);
                  }
                  finally {
                        source.close();
                  }
                  cached = new String(output.toByteArray());
                  for (Object key : skinProperties.keySet())
                        cached = cached.replaceAll("\\$" + key.toString(), skinProperties.get(key).toString());
            }
            return new ByteArrayInputStream(cached.getBytes());
      }
      
      @Override
      public String getRequestPath() {
            return original.getRequestPath();
      }
      @Override
      public Map<String, String> getResponseHeaders() {
            return original.getResponseHeaders();
      }
      @Override
      public URL getURL() {
            return original.getURL();
      }
      @Override
      public boolean userAgentNeedsUpdate(FacesContext context) {
            return original.userAgentNeedsUpdate(context);
      }
      @Override
      public String getContentType() {
            return "text/css";
      }
}

META-INF/faces-config.xml

You need to tell JSF to use your custom resource handler, you can do this in the faces-config file:
<faces-config
xmlns = "http://java.sun.com/xml/ns/javaee"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
version = "2.0">
      <application>
            <resource-handler>com.example.web.common.CSSResourceHandler</resource-handler>
      </application>
</faces-config>
At this point I'm not entirely sure how this works. Apart from the mandatory faces-config.xml file in your main war, you can apparantly have faces-config.xml files in your libraries as well. Richfaces registers its own resource handler and since both frameworks still work, JSF must register both instead of having a "the last setting wins" policy. How this plays out at runtime (are all the handlers stacked in order of appearance? Is it a flat list of handlers that is looped through?...) is unclear at this point.

Putting it together

Suppose you have a separate jar which defines some templates/components and most notably: some styling. The jar layout is as follows:
  • META-INF: everything must be in meta-inf for jsf to pick it up
    • resources: all jsf-related resources including templates must be available in the resources folder
      • style: the library "style"
        • default.compiled.css
      • templates
        • layout.xhtml
    • skins
      • custom.skin.properties
Suppose we define a very simple layout.xhtml:
<html xmlns = "http://www.w3.org/1999/xhtml"
      xmlns:h = "http://java.sun.com/jsf/html"
      xmlns:f = "http://java.sun.com/jsf/core"
      xmlns:rich = "http://richfaces.org/rich"
      xmlns:a4j = "http://richfaces.org/a4j"
      xmlns:ui = "http://java.sun.com/jsf/facelets">
      <h:head>
            <title>
                  <ui:insert name = "title"/>
            </title>
            <h:outputStylesheet name = "default.compiled.css" library = "style"/>
      </h:head>
      <h:body><ui:insert name = "main"/></h:body>
</html>
As you can see, we can reference the compiled stylesheet as we would a normal stylesheet. Now suppose we have this bit in the stylesheet:
.menu .main a {
      background: -webkit-gradient(linear, left top, left bottom, from($menuGradientLight), to($menuGradientDark));
      background: -moz-linear-gradient(top, $menuGradientLight, $menuGradientDark);
      padding: 0px 10px;
      padding-top: 8px;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
}
Then we need to add the properties to the custom.skin.properties file:
menuBorderColor=#666666
menuGradientLight=#404040
menuGradientDark=#2b2b2b
When you open the css as it is retrieved by the browser, you will see:
.menu .main a {
      background: -webkit-gradient(linear, left top, left bottom, from(#404040), to(#2b2b2b));
      background: -moz-linear-gradient(top, #404040, #2b2b2b);
      padding: 0px 10px;
      padding-top: 8px;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
}
If you package this as a "theme" library for your jsf applications, you can create an index.xhtml in your main war file that contains:
<ui:composition xmlns = "http://www.w3.org/1999/xhtml"
      xmlns:h = "http://java.sun.com/jsf/html"
      xmlns:f = "http://java.sun.com/jsf/core"
      xmlns:rich = "http://richfaces.org/rich"
      xmlns:a4j = "http://richfaces.org/a4j"
      xmlns:ui = "http://java.sun.com/jsf/facelets"
      template = "templates/layout.xhtml">
      <ui:define name = "main">
            <p>the main content comes here!</p>
      </ui:define>
</ui:composition>

Author: Alexander Verbruggen

No comments:

Post a Comment