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"
<context-param>
<param-name>org.richfaces.skin</param-name>
<param-value>test</param-value>
</context-param>
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}'
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" />
<link type = "text/css" rel = "stylesheet"
href = "/app/rfRes/myStyleSheet.ecss.xhtml?db=eAHb-CCOEQAGJQHx&ln=style" />
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;
}
*.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;
}
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
.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;
}
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;
}
}
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>
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
<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>
.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;
}
menuBorderColor=#666666
menuGradientLight=#404040
menuGradientDark=#2b2b2b
.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;
}
<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