Pages

Monday, October 25, 2010

JSF Page Fragment Caching

Problem : Our menu took ages to render.
Our application menu had links to various functions of the apps., 400+ nodes, which needed to be authorized based on EL. JSF has to execute certain EL for each node and decide if its to be rendered and than actually render the menu.
Solution : Cache it.
Simplest solution is to cache your menu. Our menu changes when the users roles changes which doesn't happen all that often so JSF doesn't have to do all that computing and since lots of user share same set of roles they can share the cached menus.
How ?
As far as I am aware Seam has a cache component. I didn't used that and I thought I could do simple component to cache with out much hassle. Here is how the tag looks like;
<mca:cache region="menu" key="#{viewUtility.getMenuKey()}">
... menu goes here ...
</mca:cache>
We need a cache region and cache key. Region will be used to specify what you are caching. Key will identify the value. In this case I used the hash code of the user's roles so that same cached will be used for different users as long as they have the same roles.
Much of the work is done on renderer here;
/**
* User: malpay
* Date: 12.Ağu.2010
* Time: 10:53:06
*/
public class CacheRenderer extends Renderer {

private final static Log log = LogFactory.getLog(CacheRenderer.class);

private CacheManager cacheManager;

public CacheManager getCacheManager() {
if (cacheManager == null) {
cacheManager = (CacheManager) FacesContextUtils.getWebApplicationContext(
FacesContext.getCurrentInstance()).getBean("ehCacheManager");
}
return cacheManager;
}


private void replaceResponseWriter(FacesContext context) {
ResponseWriter rw = context.getResponseWriter();
CacheWriter cw = new CacheWriter(rw);
context.setResponseWriter(cw);
}

private boolean cacheNotUptoDate(CacheComponent cc, Cache cache) {
return cache.get(cc.getKey()) == null;
}

@Override
public void encodeBegin(FacesContext context, UIComponent component) throws IOException {
CacheComponent cc = (CacheComponent) component;
Cache cache = getRegion(cc.getRegion());
if (cacheNotUptoDate(cc, cache)) {
replaceResponseWriter(context);
}
}

@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
CacheComponent cc = (CacheComponent) component;
if (responseWriterReplaced(context)) {
CacheWriter cw = (CacheWriter) context.getResponseWriter();
String value = updateCache(cc, cw);
context.setResponseWriter(cw.getResponseWriter());
context.getResponseWriter().write(value);
}
}

@Override
public void encodeChildren(FacesContext context, UIComponent component) throws IOException {
CacheComponent cc = (CacheComponent) component;
Cache cache = getRegion(cc.getRegion());
if (cacheNotUptoDate(cc, cache)) {
for (UIComponent child : component.getChildren()) {
child.encodeAll(context);
}
} else {
char[] chars = cache.get(cc.getKey()).getValue().toString().toCharArray();
log.info("rendering cache region : " + cc.getRegion() + "" + cc.getKey());
context.getResponseWriter().write(chars, 0, chars.length);
}
}

private String updateCache(CacheComponent cc, CacheWriter cw) {
Cache cache = getRegion(cc.getRegion());
String value = cw.getValue();
cache.put(new Element(cc.getKey(), value));
return value;
}

private boolean responseWriterReplaced(FacesContext context) {
return context.getResponseWriter() instanceof CacheWriter;
}


private Cache getRegion(String region) {
if (getCacheManager().getCache(region) == null) {
getCacheManager().addCache(region);
}
return getCacheManager().getCache(region);
}

@Override
public boolean getRendersChildren() {
return true;
}
}
What it does is check the cache if its up to date, if not render the children as usually but save the rendered portion of the page and update the cache with it. If the cache is up to date don't render the children, simply wrote the cache on the response.
This works well on my scenario but be aware It may require some adjustments for more general use.