A fluent-API-style builder is a nice way to create objects. I wrote about it in my previous post. Unfortunately this pattern does not work well together with JSF, because your domain model needs JavaBean-style properties if you want to use it in the expression language (EL) of JSF.
Accessing objects via EL
In fact this is not the whole truth. JSF provides many possibilities to access properties via EL: Besides properties of JavaBeans, you can access content of arrays, lists and maps - and most notably it provides a mechanism to extend this to your own need: The ELResolver. In general an ELResolver is responsible for the evaluation of expressions like #{address.city}
(which could also be written as #{address['city']}
). So, if we want to achieve that this expression matches a fluent-style Builder (i.e. calls a method like AddressBuilder.withCity(String)
), we have to implement an ELResolver ourself.
Extending JSF via a custom ELResolver
The ELResolver itself is an abstract class with some abstract methods, that we have to implement, when we subclass an ELResolver.
JSF maintains a list of ELResolvers. When an expression is encountered, it is split at the dots (the actual parsing is a bit more complicated, since square brackets are allowed, too) and for every part of that expression the appropriate ELResolver is searched by walking through the chain and looking for the first ELResolver that feels responsible for that part of the expression.
A FluentELResolver
So how to write an ELResolver that accepts objects with a fluent api?
Let's take the example from my previous post: We have an AddressBuilder
with a property streetNumber
and a property street
, that can be set by calling forStreetNumber(String).ofStreet(String)
.
In JSF we want to bind our input fields to the expressions #{address.streetNumber}
and #{address.street}
. The first part is easy. We have to create a CDI bean from our AddressBuilder
that is accessible via the name "address". We can achieve this by adding @Named and a scope to the builder. I choose the conversation scope here:
@Named("address")
@ConversationScoped
public class AddressBuilder {
...
}
So when JSF encounters the expression #{address.street}
it will first ask all ELResolver
s, if they know an object of name "address". This will be done by calling the method getValue(ELContext context, Object base, Object property)
of every ELResolver
with the base parameter being null
and the property parameter being the string "address". Well, our ELResolver
does not have to care about resolving an object with name "address", since CDI will resolve it and return the appropriate bean. But then it comes to the part "street". Again JSF will call the method getValue(ELContext context, Object base, Object property)
of every ELResolver. This time the base will be our AddressBuilder
and the property will be "street". So what to do in our FluentELResolver
? For the first shot, let's assume, that we have a getter for every property in our AddressBuilder
. For this case, we could just extend an existing ELResolver, the javax.el.BeanELResolver
and JSF will accept our builder to be responsible for the expression. The BeanELResolver
is responsible for evaluation of JavaBean style properties. Later on I will discuss what to do, if we don't want our builder to contain getters. So for now the only difference of our FluentELResolver
to the javax.el.BeanELResolver
are the write operations. For this we have to override the methods boolean isReadOnly(ELContext context, Object base, Object property)
and void setValue(ELContext context, Object base, Object property, Object value)
. Within this methods we have to check, if a method exists on the base object, that may be a fluent api method of the property:
public FluentELResolver extends BeanELResolver {
public boolean isReadOnly(ELContext context,
Object base,
Object property) {
return getFluentMethod(context,
base,
property.toString())
!= null;
}
public void setValue(ELContext context,
Object base,
Object property,
Object value) {
invoke(getFluentMethod(context,
base,
property.toString),
base,
value);
}
private Method getFluentMethod(ELContext context,
Object base,
String name) {
name = Character.toUpperCase(name.charAt(0))
+ name.substring(1);
for (Method method: base.getClass().getMethods()) {
if (isFluentWriteMethod(method,
context,
base,
name)) {
return method;
}
}
return null;
}
private boolean isFluentWriteMethod(Method method,
ELContext context,
Object base,
String property) {
return method.getName().endsWith(property)
&& method.getParameterTypes().length == 1
&& method.getParameterTypes()[0]
== getType(context, base, property);
}
}
Of course, for performance reasons the result of the method getFluentMethod
should be cached. This is left as an excercise for the reader. Besides that the code is still not complete. As I mentioned earlier, JSF will walk through every ELResolver
to ask, if it can handle a certain property. But how can JSF know, if our resolver has handled the property? We have to inform the ELContext
. This can be done by calling context.setPropertyResolved(true)
. So we add this call and some null checks to the code:
public FluentELResolver extends BeanELResolver {
public boolean isReadOnly(ELContext context,
Object base,
Object property) {
if (base == null || !(property instanceof String)) {
return true;
}
Method method = getFluentMethod(context,
base,
property.toString());
if (method == null) {
return true;
}
context.setPropertyResolved(true);
return false;
}
public void setValue(ELContext context,
Object base,
Object property,
Object value) {
if (base == null || !(property instanceof String)) {
return;
}
Method method = getFluentMethod(context,
base,
property.toString());
if (method == null) {
return;
}
invoke(method, base, value);
context.setPropertyResolved(true);
}
private Method getFluentMethod(ELContext context,
Object base,
String name) {
...
}
private void invoke(Method method,
Object base,
Object value) {
try {
method.invoke(base, value);
} catch (IllegalAccessException e) {
throw new ELException(e);
} catch (InvocationTargetException e) {
throw new ELException(e.getCause());
}
}
}
Registering the ELResolver
The registration of our FluentELResolver
is very simple. Just put it into the faces-config.xml:
<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>
<el-resolver>
de.openknowledge.extensions.el.FluentELResolver
</el-resolver>
</application>
</faces-config>
Using builders without getters
Until this point, we assumed that our builder has getter methods to read-access the fields we want to set via the fluent api. What, if we don't have getters? If we want to do that, we no longer need to extend BeanELResolver
, but can directly extend ELResolver
and implement the remaining methods (most notable getValue
) on ourselfs.
For the implementation of getValue
I suggest scanning the available fields in the class by reflection. This should work for most cases. If you use the Inner Class Builder from my previous post, you should also scan the declaring class of the builder class and you may scan it for getters, too.
From a technical point of view, it would be possible to implement an ELResolver
, that completely relies on reflection (also for the method setValue
). I.e. we could implement a FieldAccessELResolver
. Please don't do this! You would loose every validity check, that is implemented in your builder, since its methods are not used any more. Since you usually don't have validity checks in your getters, it seems to be ok, to use field access there. Btw. a FieldAccessELResolver
as well as the field-access approach suggested above will get problems, if you deal with a contextual reference of CDI, since it is just a proxy and will not have its fields set.
The german version of this post can be found here.