Wednesday, May 17, 2006

Spring Hacks : Reflection Based Property Editors.

Property Editors are a J2SE feature used for editing JavaBeans properties from user interfaces, typically representing these properties as text values.

They are used in SpringMVC for similar purposes: representing and binding complex objects in web interfaces as they were simple, plain, properties.

A short example will clarify.
Say you have an Order owning a collection of Orders, each with its Product:


public class Order {

public String getOrderCode() { ... }

public List getLineItems() { ... }
}


public class LineItem {

public String getItemCode() { ... }

public Product getProduct() { ... }
}


public class Product {

public String getCode() { ... }

public String getName() { ... }
}



If you want to display Products in a web selection list, in order to assign a Product to a LineItem, you have to write in your JSP something like this:


<spring:bind path="command.product">
<select name="${status.expression}">
<c:forEach items="${products}" var="p">
<spring:transform value="${p}" var="pCode"/>
<option value="${pCode}">${p.name}</option>
</c:forEach>
</spring:bind>


For doing this, you have to write and configure a PropertyEditor for the Product object, in order to transform the Product object into a text value, specifically its code, and then directly bind the Product having its text representation.
The same applies if you want to bind a collection of LineItems to a Product:


<spring:bind path="command.lineItems">
<c:forEach items="${items}" var="item">
<input type="checkbox" name="${status.expression}" value="${item.itemCode}"/>
.....
</c:forEach>
</spring:bind>


You have to write a CustomCollectionEditor for the collection of LineItem objects, in order to convert the collection of line item codes to the corresponding collection of LineItems objects (take a look here for more info).

Writing PropertyEditors or CustomCollectionEditors is pretty simple: however, if you have to do a lot of bindings, each for a different object, you have to hand-write a lot of property editors, which is boring and error-prone.

Now, think a moment at how you usually transform objects to text values and vice-versa: you use an object property for obtaining a text representation (often an id), and use some kind of data access object for obtaining an object from a given text value (i.e., an object given its id).
So, let us write a generic property editor and a generic collection editor, using Java Reflection.
Here is the property editor:


public class ReflectivePropertyEditor extends PropertyEditorSupport {

private Object dataAccessObject;
private String dataAccessMethod;
private String propertyName;
private PropertyEditor stringConvertor;

public String getAsText() {
if (this.getValue() == null) {
return null;
}
else {
Method method = null;
String result = null;
try {
BeanWrapperImpl wrapper = new BeanWrapperImpl(this.getValue());
result = wrapper.getPropertyValue(this.propertyName).toString();
}
catch(Exception ex) {
throw new ReflectiveCollectionEditorException("An error occurred while using: " + this.propertyName, ex);
}
return result;
}
}

public void setAsText(String textValue) {
try {
if (this.stringConvertor == null) {
Method method = this.dataAccessObject.getClass().getMethod(this.dataAccessMethod, new Class[]{String.class});
this.setValue(method.invoke(this.dataAccessObject, new Object[]{textValue}));
}
else {
this.stringConvertor.setAsText(textValue);
Object value = this.stringConvertor.getValue();
Method method = this.dataAccessObject.getClass().getMethod(this.dataAccessMethod, new Class[]{value.getClass()});
this.setValue(method.invoke(this.dataAccessObject, new Object[]{value}));
}
}
catch(Exception ex) {
throw new ReflectiveCollectionEditorException("An error occurred while executing: " + this.dataAccessMethod, ex);
}
}

public String getPropertyName() {
return this.propertyName;
}

public void setPropertyName(String propertyName) {
this.propertyName = propertyName;
}

public Object getDataAccessObject() {
return this.dataAccessObject;
}

public void setDataAccessObject(Object dataAccessObject) {
this.dataAccessObject = dataAccessObject;
}

public String getDataAccessMethod() {
return this.dataAccessMethod;
}

public void setDataAccessMethod(String dataAccessMethod) {
this.dataAccessMethod = dataAccessMethod;
}

public PropertyEditor getStringConvertor() {
return this.stringConvertor;
}

public void setStringConvertor(PropertyEditor stringConvertor) {
this.stringConvertor = stringConvertor;
}
}


And here the collection editor:


public class ReflectiveCollectionEditor extends CustomCollectionEditor {

private Object dataAccessObject;
private String dataAccessMethod;
private PropertyEditor stringConvertor;

public ReflectiveCollectionEditor(Class collection) {
super(collection);
}

public ReflectiveCollectionEditor(Class collection, boolean nullAsEmptyCollection) {
super(collection, nullAsEmptyCollection);
}

protected Object convertElement(Object element) {
if (!(element instanceof String)) {
new IllegalArgumentException("The element to convert must be of type java.lang.String");
}
String textValue = (String) element;
Object result = null;
try {
if (this.stringConvertor == null) {
Method method = this.dataAccessObject.getClass().getMethod(this.dataAccessMethod, new Class[]{String.class});
result = method.invoke(this.dataAccessObject, new Object[] {textValue});
}
else {
this.stringConvertor.setAsText(textValue);
Object value = this.stringConvertor.getValue();
Method method = this.dataAccessObject.getClass().getMethod(this.dataAccessMethod, new Class[]{value.getClass()});
result = method.invoke(this.dataAccessObject, new Object[] {value});
}
}
catch(Exception ex) {
throw new ReflectiveCollectionEditorException("An error occurred while executing: " + this.dataAccessMethod, ex);
}

return result;
}

public Object getDataAccessObject() {
return this.dataAccessObject;
}

public void setDataAccessObject(Object dataAccessObject) {
this.dataAccessObject = dataAccessObject;
}

public String getDataAccessMethod() {
return this.dataAccessMethod;
}

public void setDataAccessMethod(String dataAccessMethod) {
this.dataAccessMethod = dataAccessMethod;
}

public PropertyEditor getStringConvertor() {
return this.stringConvertor;
}

public void setStringConvertor(PropertyEditor stringConvertor) {
this.stringConvertor = stringConvertor;
}
}


What they ask you to do is nothing more than configuring the following properties:

  • dataAccessObject : the object used for converting from a text value to the actual object; it could be a Factory, a DAO, or Repository.

  • dataAccessMethod : the method, on the dataAccessObject, to call for converting from text to object.

  • propertyName : the name of the property which will be used for the object text value (only in ReflectivePropertyEditor).

  • stringConvertor : a PropertyEditor to use for converting the string value that will be passed as argument to the dataAccessMethod (required only if it takes as argument an object that is not a String).


Then, Java Reflection will do all the boring job.

Moreover, those property editors are best configured in the Spring container:


<bean id="productEditor" class="org.acme.ReflectivePropertyEditor">
<property name="dataAccessObject"><ref bean="productDAO"/></property>
<property name="dataAccessMethod"><value>getProduct</value></property>
<property name="propertyName"><value>code</value></property>
</bean>

<bean id="lineItemsCollectionEditor" class="org.acme.ReflectiveCollectionEditor">
<constructor-arg index="0"><value>java.util.Set</value></constructor-arg>
<property name="dataAccessObject"><ref bean="productDAO"/></property>
<property name="dataAccessMethod"><value>getLineItem</value></property>
</bean>



Comments and suggestions are welcome.
Hope you'll find this useful!

1 comment:

Unknown said...

A really helpful article - Thank you very much I wish you don't mind me writing about this post on my website I will also link back to this post Thanks
Palacio cluster house