Saturday, July 30, 2005

A strategy for binding "complex" properties in Cocoon Forms

First, a short introduction about the problem context.

Cocoon Forms provides a binding framework for both loading object properties into form widgets and saving form values into object properties.
This is done through the association of every form widget you want to bind, with a JXPath expression leading to a particular object property.
This is an XML snippet showing a binding example:

<fb:value id="name" path="name"/>
<fb:value id="street" path="address/street"/>

We associate the value of the "name" widget with the "name" property of some object passed by Cocoon control flow, and the "street" widget value with the "street" property contained by the object "address" property.
For bigger clarity, this is a code snippet for the object used in our binding example:

public class Person {
private String name;
...
private Address address;
...
}

And this is its Address property:

public class Address {
private String street;
...
}

The address property is a sort of "complex", or "composite", property.

The problem arises when you want to save form values into a Person object just created, which has a null address property: this will throw a JXPathException because the JXPath context used by Cocoon doesn't know how to create the address property.
So, the following binding:

<fb:value id="street" path="address/street"/>

Will not work.
And, like it, the binding of every complex property with a default null value.

Solutions?

A first one could be to set the address property to a new Address into the Person constructor or in Cocoon control flow, but this is very poor, because requires you to change the business code, or hack the control flow.

A better solution is to use JXPath factories and Cocoon custom binding.
Let me explain my idea.

First of all, create a JXPath factory for making Address objects; this is very simple:

public class AddressFactory extends AbstractFactory {

public boolean createObject(JXPathContext context, Pointer pointer, Object parent,
String name, int index) {

pointer.setValue(new Address());

return true;
}
}

The factory must be set in the JXPath context object, which will use the createObject() method for creating null properties, like "address" in our example (for additional details see the JXPath User Guide and its javadoc).

The question is: how to do that in Cocoon?
The answer is, you can guess it, in custom binding.

First, configure it:

<fb:custom id="street" path="." builderclass="com.example.JXPathConfiguratorFactory" factorymethod="makeConfigurator">
<fb:config relative="address/street" factory="com.example.AddressFactory"/>
</fb:custom>

JXPathConfiguratorFactory is the factory which will make JXPathConfigurator objects through its method "makeConfigurator".
It must be configured, through the "fb:config" element, with two attributes:

  1. The JXPath expression relative to the "path" attribute. In our example this is called "relative".
  2. The fully qualified class name of the JXPath factory (previously shown) to use, here called "factory".

Important: why do we need two different "path" and "relative" attributes?
The reason is that if you set the JXPath expression in the "path" attribute of the "fb:custom" element, Cocoon will try to create the path BEFORE calling the custom binding, causing the well known JXPathException.
So, we must set the "path" attribute to the current context path (which should be existent), and the "relative" path to the desired JXPath expression.

Finally, write the JXPathConfiguratorFactory, as you usually do, and the JXPathConfigurator:

public class JXPathConfigurator extends AbstractCustomBinding {

private Element config;

...

protected void doSave(Widget widget, JXPathContext context) throws Exception {

JXPathContext configContext = JXPathContext.newContext(config);
String relative = (String) configContext.getValue("@relative");
String factory = (String) configContext.getValue("@factory");

context.setFactory((org.apache.commons.jxpath.AbstractFactory) Class.forName(factory).newInstance());

context.createPathAndSetValue(relative,widget.getValue());
}
}

It will simply set the factory into the JXPathContext object, create the null property and set the widget value!

I think that this solution has mainly three advantages:

  1. Non-invasiveness : it doesn't require you to change business code or control flow, but only your binding configuration file.
  2. Reusable : AddressFactory can be used whenever you need a property of type "Address", and the JXPathConfigurator can be used with every JXPath factory.
  3. Simple.

I'd like to know your opinion and ideas.

And, as usual ... have a good coding!

No comments: