Wednesday, June 14, 2006

Creating composite controls with JSF and facelets

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).

20 comments:

Anonymous said...

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

Anonymous 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

Anonymous 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.

Anonymous 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?

Anonymous 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 .

Anonymous 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.

Anonymous 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?

Anonymous 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?

Anonymous 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.

Anonymous 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 :(

Anonymous 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!

Anonymous 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!

luiggitama said...

Hi Andrew:
I'm having trouble with your code. My composite component should render a h:commandButton, but I want to send an action="startTask", which should trigger a page navigation.
I am debugging javax.faces.component.UICommand.getAction() and it gets "#{action}" as the expression, so the navigation is not triggered.
How should I tune your code so this works?
Regards,
Luis

Unknown said...

I never had the code check for a literal string. The ValueExpression object in 1.2 has an isLiteralText() function, so you can check that.

So in this line:
MethodExpression mexpr = buildMethodExpression(ctx, currentExpression.getExpressionString(), methodData);

Before that line, check for isLiteralText() on the currentExpression, if it is, don't create the method expression and set it on the variable mapper (leave the literal expression alone). That should work or something similar.

markus said...

Hi Andrew,
thank you for this nice solution. But we trapped in another problem with the compositeControl-Component: All things go well if an action method (could be an action method of any component of the view) returns a String representing a navigation rule. But if the outcome of the method is null to redisplay the current page not all components will rendered. The components in the same Parent-Component as the composition are not in the children-list of the parent anymore if the composition comes after them.
The problem occurs only if we use the compositeControl component inside a composition component's xhtml. If we use compositeControl in a "normal" facelets xhtml it's working just fine even if an action method returns null.
Example:
In this xhtml it works just fine with an action outcome == null. All will be rendered. (The compositeControl doesn’t makes sense here, but it’s for seeing the difference):

….
<ui:composition template="/layout/template.xhtml">
<ui:define name="main">

<tr:panelFormLayout id="test" labelWidth="40%" fieldWidth="60%">
<tr:inputText id="normal Component" value="#someBean.someAttribute" label="normal TextInput" />

<dz:compositeControl
methodBindings="action=java.lang.String;”>
<tr:commandLink text="test" action="#{dummyBean.action}" />
</dz:compositeControl>
</tr:panelFormLayout >

</ui:define>
</ui:composition>


In this example tr:inputText will not rendered any more if the action’s outcome is null.

….
<ui:composition template="/layout/template.xhtml">
<ui:define name="main">

<tr:panelFormLayout id="test" labelWidth="40%" fieldWidth="60%">
<tr:inputText id="normal Component" value="#someBean.someAttribute" label="normal TextInput" />

<dz:testComposition action=”#{dummyBean.action}” />
</tr:panelFormLayout >

</ui:define>
</ui:composition>


Whereas dz:testComposition is a composition component:

<ui:composition>
<dz:compositeControl
methodBindings="action=java.lang.String">
<tr:commandLink text="test"
action="#{action}"/>
</dz:compositeControl>
</ui:composition>


We are using trinidad & shale but I don't think that's the reason for the problem.

Any ideas?
Thank you very much in advance.
Regards,
Markus

Unknown said...

@Markus:

It is hard to say what may be going on for you without a test case to try to test it with. Can you debug into the test code and see if the component is still there, or if it got removed? Perhaps my nested tag handlers are causing the problem.

FYI: JSF is providing support for component components, so there is relief in site for not having to hack around this.

Unknown said...

I found a solution based on Andrew's solution, but it's simpler and cleaner, in my opinion. It´s the ActionMapperTagHandler from the richfaces photoalbum example app.

See the link:

http://docs.jboss.org/richfaces/latest_3_3_X/en/realworld/html/Button.html