Wednesday, June 14, 2006

Creating composite controls with JSF and facelets

My blog has moved, please update your links. You can find this article here

JSF, although a powerful framework does not have many tools to assist in the development of composite controls. Whether building a control that has a few controls in it or a control comprised of 100s of children, there is no easy solution. The JSF specification was more written to build components that render themselves entirely.

With that said Facelets is a great add on to JSF and has some great templating features. Using a user tag (a.k.a. source tag), a composite control can be easily created. The trouble is that it is not possible out-of-the-box to pass method bindings to children components.

For example:
Snippet from taglib.xml:
<tag> <tag-name>test</tag-name> <source>tags/testTag.xhtml</source> </tag>
Usage in an XHTML file:
<my:test actionListener="#{myBean.doSomething}" />
User Tag file:
<ui:composition> <h:commandButton value="Click Me" actionListener="#{actionListener}" /> </ui:composition>

The problem with the above code is that the user tag handler of facelets always creates ValueExpression objects for each attribute in the source tag. This is fine for properties, but in the above case, a MethodExpression is what "ought" to be created.

I wanted to solve this problem, but in such a way to avoid people having to create new tag handlers or component handlers. I wanted a re-usable complete solution. After dragging myself through the mire of source code, I found what I needed in the facelets API to extend it. My solution is two part:

  1. Create a tag handler with component support
  2. Create a new value expression that returns method expressions

Creating a value expression that is a method expression

The second step above is easiest to discuss first. The idea is to have a value expression that returns a method expression as its value. This will allow "#{myBean.doSomething}" to be interpreted as a method instead of a property called "getDoSomething"

The code below may need some work to be more "correct", but it does work:

import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import javax.el.ELContext; import javax.el.MethodExpression; import javax.el.ValueExpression; public class MethodValueExpression extends ValueExpression implements Externalizable { private ValueExpression orig; private MethodExpression methodExpression; public MethodValueExpression() {} MethodValueExpression(ValueExpression orig, MethodExpression methodExpression) { this.orig = orig; this.methodExpression = methodExpression; } @Override public Class getExpectedType() { return orig.getExpectedType(); } @Override public Class getType(ELContext ctx) { return MethodExpression.class; } @Override public Object getValue(ELContext ctx) { return methodExpression; } @Override public boolean isReadOnly(ELContext ctx) { return orig.isReadOnly(ctx); } @Override public void setValue(ELContext ctx, Object val) {} @Override public boolean equals(Object val) { return orig.equals(val); } @Override public String getExpressionString() { return orig.getExpressionString(); } @Override public int hashCode() { return orig.hashCode(); } @Override public boolean isLiteralText() { return orig.isLiteralText(); } /** * @see java.io.Externalizable#readExternal(java.io.ObjectInput) */ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { orig = (ValueExpression)in.readObject(); methodExpression = (MethodExpression)in.readObject(); } /** * @see java.io.Externalizable#writeExternal(java.io.ObjectOutput) */ public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(orig); out.writeObject(methodExpression); } }

Now that we have a value expression that will wrap a method expression, we need to be able to "replace" them into the JSF environment. When facelets is applying components to the tree, it has an EL context that can be used to interpret data. If we know the method signatures of the methods to bind to, we will be able to create MethodExpression objects.

Lets start with the constructor. We want to create an attribute that will be our configuration. Unfortunately, an attribute can only be given once, so I will create a custom format that we will be able to use. Starting code:

public class CompositeControlHandler extends TagHandler { private final TagAttribute methodBindings; private ComponentHandler componentHandler; /** * @param config */ public CompositeControlHandler(TagConfig config) { super(config); methodBindings = getAttribute("methodBindings"); } // TODO... }

We now have a skeleton in which we can declare a "methodBindings" attribute of our custom tag. This will be used to define which variables in the scope of our user tag should be considered methods instead of variables. I have choosen the following format:

attribute-name=java-return-type first-java-parameter-type second-java-parameter-type; second-attribute-name=java-return-type first-java-parameter-type second-java-parameter-type;
Example from above:
actionListener=void javax.faces.event.ActionEvent;

So now that we have a way to specify what attributes are now methods, we need to do the work.

Steps:
  1. Parse the attribute
  2. For each attribute, see if the value is "bound" in the current variable mapper
  3. If bound, create a new method expression from the configuration information
  4. Hide the initial variable with the method expression

The resultant code is as follows:

public class CompositeControlHandler extends TagHandler { private final static Pattern METHOD_PATTERN = Pattern.compile( "(\\w+)\\s*=\\s*(.+?)\\s*;\\s*"); private final TagAttribute methodBindings; /** * @param config */ public CompositeControlHandler(TagConfig config) { super(config); methodBindings = getAttribute("methodBindings"); } /** * @see com.sun.facelets.FaceletHandler#apply(com.sun.facelets.FaceletContext, javax.faces.component.UIComponent) */ public void apply(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, FaceletException, ELException { VariableMapper origVarMap = ctx.getVariableMapper(); try { VariableMapperWrapper variableMap = new VariableMapperWrapper(origVarMap); ctx.setVariableMapper(variableMap); if (methodBindings != null) { String value = (String)methodBindings.getValue(ctx); Matcher match = METHOD_PATTERN.matcher(value); while (match.find()) { String var = match.group(1); ValueExpression currentExpression = origVarMap.resolveVariable(var); if (currentExpression != null) { try { FunctionMethodData methodData = new FunctionMethodData( var, match.group(2).split("\\s+")); MethodExpression mexpr = buildMethodExpression(ctx, currentExpression.getExpressionString(), methodData); variableMap.setVariable(var, new MethodValueExpression( currentExpression, mexpr)); } catch (Exception ex) { throw new FacesException(ex); } } } } // TODO: will do this next } finally { ctx.setVariableMapper(origVarMap); } } private MethodExpression buildMethodExpression(FaceletContext ctx, String expression, FunctionMethodData methodData) throws NoSuchMethodException, ClassNotFoundException { return ctx.getExpressionFactory().createMethodExpression(ctx, expression, methodData.getReturnType(), methodData.getArguments()); } private class FunctionMethodData { private String variable; private Class returnType; private Class[] arguments; FunctionMethodData(String variable, String[] types) throws ClassNotFoundException { this.variable = variable; if ("null".equals(types[0]) || "void".equals(types[0])) returnType = null; else returnType = ReflectionUtil.forName(types[0]); arguments = new Class[types.length - 1]; for (int i = 0; i < arguments.length; i++) arguments[i] = ReflectionUtil.forName(types[i + 1]); } public Class[] getArguments() { return this.arguments; } public void setArguments(Class[] arguments) { this.arguments = arguments; } public Class getReturnType() { return this.returnType; } public void setReturnType(Class returnType) { this.returnType = returnType; } public String getVariable() { return this.variable; } public void setVariable(String variable) { this.variable = variable; } } }

Now, that we have has this much fun, why not instead of just having a tag handler, but a component handler as well. The next steps will allow this user tag to be used without any XML configuration. The goal is to allow the user to specify the component type and renderer type for a component that should be created for our user tag (If none is given, the ComponentRef from facelets will be used).

The code isn't much different, so I will show it in its entirety here:

public class CompositeControlHandler extends TagHandler { private final static Pattern METHOD_PATTERN = Pattern.compile( "(\\w+)\\s*=\\s*(.+?)\\s*;\\s*"); private final TagAttribute rendererType; private final TagAttribute componentType; private final TagAttribute methodBindings; private ComponentHandler componentHandler; /** * @param config */ public CompositeControlHandler(TagConfig config) { super(config); rendererType = getAttribute("rendererType"); componentType = getAttribute("componentType"); methodBindings = getAttribute("methodBindings"); componentHandler = new ComponentRefHandler(new ComponentConfig() { /** * @see com.sun.facelets.tag.TagConfig#getNextHandler() */ public FaceletHandler getNextHandler() { return CompositeControlHandler.this.nextHandler; } public Tag getTag() { return CompositeControlHandler.this.tag; } public String getTagId() { return CompositeControlHandler.this.tagId; } /** * @see com.sun.facelets.tag.jsf.ComponentConfig#getComponentType() */ public String getComponentType() { return (componentType == null) ? ComponentRef.COMPONENT_TYPE : componentType.getValue(); } /** * @see com.sun.facelets.tag.jsf.ComponentConfig#getRendererType() */ public String getRendererType() { return (rendererType == null) ? null : rendererType.getValue(); } }); } /** * @see com.sun.facelets.FaceletHandler#apply(com.sun.facelets.FaceletContext, javax.faces.component.UIComponent) */ public void apply(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, FaceletException, ELException { VariableMapper origVarMap = ctx.getVariableMapper(); try { VariableMapperWrapper variableMap = new VariableMapperWrapper(origVarMap); ctx.setVariableMapper(variableMap); if (methodBindings != null) { String value = (String)methodBindings.getValue(ctx); Matcher match = METHOD_PATTERN.matcher(value); while (match.find()) { String var = match.group(1); ValueExpression currentExpression = origVarMap.resolveVariable(var); if (currentExpression != null) { try { FunctionMethodData methodData = new FunctionMethodData( var, match.group(2).split("\\s+")); MethodExpression mexpr = buildMethodExpression(ctx, currentExpression.getExpressionString(), methodData); variableMap.setVariable(var, new MethodValueExpression( currentExpression, mexpr)); } catch (Exception ex) { throw new FacesException(ex); } } } } componentHandler.apply(ctx, parent); } finally { ctx.setVariableMapper(origVarMap); } } private MethodExpression buildMethodExpression(FaceletContext ctx, String expression, FunctionMethodData methodData) throws NoSuchMethodException, ClassNotFoundException { return ctx.getExpressionFactory().createMethodExpression(ctx, expression, methodData.getReturnType(), methodData.getArguments()); } private class FunctionMethodData { private String variable; private Class returnType; private Class[] arguments; FunctionMethodData(String variable, String[] types) throws ClassNotFoundException { this.variable = variable; if ("null".equals(types[0]) || "void".equals(types[0])) returnType = null; else returnType = ReflectionUtil.forName(types[0]); arguments = new Class[types.length - 1]; for (int i = 0; i < arguments.length; i++) arguments[i] = ReflectionUtil.forName(types[i + 1]); } public Class[] getArguments() { return this.arguments; } public void setArguments(Class[] arguments) { this.arguments = arguments; } public Class getReturnType() { return this.returnType; } public void setReturnType(Class returnType) { this.returnType = returnType; } public String getVariable() { return this.variable; } public void setVariable(String variable) { this.variable = variable; } } }

Now we need to register this in a taglib.xml so that we can use it:

<tag> <tag-name>compositeControl</tag-name> <handler-class>mypackage.CompositeControlHandler</handler-class> </tag>
Now that it is registered, lets use it. The XHTML file that uses the tag hasn't changed:
<my:test actionListener="#{myBean.doSomething}" />
The user tag does look different, but not that much:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:my="http://mynamespace"> <body> <ui:composition> <my:compositeControl id="#{id}" methodBindings="action=java.lang.String; actionListener=void javax.faces.event.ActionEvent;"> <ui:debug /> <h:commandButton value="Click me" actionListener="#{actionListener}" action="#{action}" /> </my:compositeControl> </ui:composition> </body> </html>
That should be enough to get you going. © Copyright 2006 - Andrew Robinson. Please feel free to use in your applications under the LGPL license (http://www.gnu.org/licenses/lgpl.html).

17 comments:

Sebastián Fiorentini said...

Great solution. Maybe it can be added to contrib section of facelets site no?

Andrew said...

To be honest, I am hoping that Jacob or another Facelet author picks this up, integrates in into the facelets library and makes it more elegant than my current solution here. This was more of a "quick" solution to get what I needed working.

Anonymous said...

excellent article!

Anonymous said...

Great article

stephane said...

Hi,

Good solution but as you say in your comment it's a "quick" solution.

We try to write our custom component in my company. We have choiced facelets-ajax4jsf-tomahawk-sandbox to write those composite components. But we have had same problems like you. Your solution is good but too simple for our needs.

I've searched other solution but I not found. Also I would know if you have improved your solution or ,better, if facelets have solved this problem in another release?

Thanks Stephane.

Andrew said...

Stephanie,

I am using this solution in a production environment. The syntax is not what I would prefer for a facelets component, but it is fine for our own needs. I have been hoping that facelets would adopt this idea, but they haven't yet.

You say the solution is too simple for your needs, how so? What functionality is it lacking?

stephane said...

Thanks for your answer.

Your solution is too simple for us because we must create too many tags to use it.It's not enough "generics".
However we didn't found another solution and we will use your hack.

Regards .

stephane said...

Andrew,

We have found another solution for the problem.
There are a specific synthax to pass action to composite controls.
This synthax is here :
http://www-128.ibm.com/developerworks/java/library/j-facelets/#N103FE

Regards. Stephane.

Andrew said...

Yes that is an option, but IMO, a horrible hack. "Normal" JSF allows methods to be put into action and actionListener attributes, why should a facelets composite control be any different?

David said...

Richard Hightower's solution (linked by stephane) is far more elegant if you ask me. The real problem is in delaying execution of an action method. Rather than hack in tag handlers and your own reflective evaluators, why not just use the reflective action="#{backingBean[action]}" EL syntax to accomplish the same thing?

Andrew said...

I believe I already commented on that. Richard's solution is a hack and is not part of the JSF standard way of passing methods. For example:

<h:commandButton action="#{myBean.myAction}" />

is not written like:

<h:commandButton actionBean="#{bean}" action="myAction" />

This is ugly and it is obvious that your component is not a JSF component, but instead a facelet source file hack. It is un-elegant IMO and is therefore not an option.

JSF components should be a black box, your users should not have to code something in a special way because you decided to use facelets instead of Java to write your component.

David said...

To hack, or not to hack. I agree that the bean[action] solution isn't very JSF-like, and your solution is complete in that sense. But it works, and for us it's not an issue since all our components are Facelets compositions, inherently 'not' black-box since you currently can't define necessary attributes for your composite components other than in your own documentation. So that's the least of our worries :P

In defining action vs. value attributes for a composite component, IMO the nicest solution might be to define attributes in the facelets taglib.xml (tld-style) rather than into the composition page itself. Pity that doesn't exist yet :(

bwlee said...

nice solution!

And I am trying to use it.

But I trapped in a problem, all other things go well except sandbox components, no sandbox components can be rendered to browser, any suggestion?

Thanks in advance!

Andrew said...

First make sure you register the sandbox components correctly using a taglib.xml file. If that is correct and you are still having problems, ask the myfaces user mailing list as it would be a sandbox component problem at that point

Anonymous said...

Excellent lecture! Especially the MethodValueExpression solution came in very handy for a simular problem I was facing. It made my modular facelets more reusable as now I can even define dynamic action methods. Thanks a lot!

MyNameIsNeo said...

Hey Andrew. This is an excellent solution. I'm going to use it in my current project. However, there's just one more piece that's not yet resolved.

my:test actionListener="#{myBean.doSomething}"

In a regular JSF control, the JSF would know that actionListener refers to a method binding and it would not search for a property called doSomething in bean myBean. However, in my:test, this is not apparent and an error is thrown because myBean doesn't have a property called doSomething. How did you fix this issue. Thanks in advance.

Andrew said...

First, please use my new site at http://drewdev.blogspot.com, I will no longer be keeping this one up to date.

If you are having problems with the EL, then you probably do not have the custom component registered correctly. In my example, you will see that I replace the value expression using my buildMethodExpression function.

If you do not correctly set up all my steps, then the ValueExpression will not be replaced with the MethodExpression.

See the CompositeControlHandler code in the blog.