Well at least someone other than me is interested in this 
The first part is the composite property databinder. This extracts all request parameters that contain a '_' character and interpret them as parts of a property named by the portion of the parameter preceded by the '_' character.
Code:
package org.mooli.web.bind;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.web.bind.ServletRequestDataBinder;
/**
* {@link org.springframework.validation.DataBinder} for converting composite properties
* described by multiple property names into a single named property whose value is a
* {@link java.util.Properties} instance representing the complex state of this property value. This
* can then be used by subclasses of {@link org.mooli.web.bind.AbstractCompositePropertyEditor}
* to convert {@link java.util.Properties} instances into complex objects.
* @author dhewitt
*/
public class CompositePropertyDataBinder extends ServletRequestDataBinder {
/** The separator for complex property names. */
public static final String PROPERTY_SEPARATOR = "_";
/**
* @param target the target to bind to
* @param objectName the name of the object
*/
public CompositePropertyDataBinder(final Object target, final String objectName) {
super(target, objectName);
}
/**
* Converts complex {@link PropertyValue}s into single instances with values. In
* {@link java.util.Properties} string format. eg:
*
* <code>
* name="birthDate_year" value="1960"
* name="birthDate_month" value="10"
* name="birthDate_date" value="11"
* </code>
*
* <code>
* name="birthDate" value="year=1960\nmonth=10\ndate=11"
* </code>
* @see org.springframework.validation.DataBinder#bind(org.springframework.beans.PropertyValues)
*/
public final void bind(final PropertyValues pvs) {
MutablePropertyValues mpvs = new MutablePropertyValues(pvs);
PropertyValue[] pvArray = mpvs.getPropertyValues();
Map boundProperties = new HashMap();
for (int i = 0; i < pvArray.length; i++) {
if (pvArray[i].getName().indexOf(PROPERTY_SEPARATOR) > 0) {
bindProperty(pvArray[i], boundProperties);
}
}
mpvs.addPropertyValues(boundProperties);
super.bind(mpvs);
}
/**
* Converts complex {@link PropertyValue}s into single instances with values. In
* {@link java.util.Properties} string format. eg:
*
* <code>
* name="birthDate_year" value="1960"
* name="birthDate_month" value="10"
* name="birthDate_date" value="11"
* </code>
*
* <code>
* name="birthDate" value="year=1960\nmonth=10\ndate=11"
* </code>
*
* @param propertyValue the value to convert
* @param boundProperties the map to add the converted property to
*/
private void bindProperty(final PropertyValue propertyValue, final Map boundProperties) {
String pvName = propertyValue.getName();
String value = pvName.substring(pvName.indexOf(PROPERTY_SEPARATOR) + PROPERTY_SEPARATOR.length());
value += "=" + propertyValue.getValue();
String property = pvName.substring(0, pvName.indexOf(PROPERTY_SEPARATOR));
if (boundProperties.containsKey(property)) {
value = boundProperties.get(property) + "\n" + value;
}
boundProperties.put(property, value);
}
}
The second part is the abstract composite property editor that converts a string representation of a properties object into a properties instance for translation into a single object, and vice versa:
Code:
package org.mooli.web.bind;
import java.beans.PropertyEditorSupport;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.propertyeditors.PropertiesEditor;
/**
* {@link java.beans.PropertyEditor} for converting a string representation of a
* {@link java.util.Properties} object into an arbitrary instance. Eg. could be used
* to convert several fields corresponding to a single date into a single instance
* of {@link Date}.
* @author dhewitt
*/
public abstract class AbstractCompositePropertyEditor extends PropertyEditorSupport {
/** Logging support. */
private final Log logger = LogFactory.getLog(getClass());
/** Whether to ignor errors when binding a composite property. */
private final boolean ignoreError;
/**
* @param ignoreError
*/
public AbstractCompositePropertyEditor() {
this(false);
}
/**
* @param ignore if true, errors (such as missing or invalid composite properties)
* should be ignored.
*/
public AbstractCompositePropertyEditor(final boolean ignore) {
super();
this.ignoreError = ignore;
}
/**
* @see java.beans.PropertyEditor#setAsText(java.lang.String)
*/
public final void setAsText(final String text) throws IllegalArgumentException {
super.setValue(null);
try {
PropertiesEditor propertiesEditor = new PropertiesEditor();
propertiesEditor.setAsText(text);
Properties properties = (Properties)propertiesEditor.getValue();
Object value = convertProperties(properties);
super.setValue(value);
} catch (IllegalArgumentException e) {
if (!ignoreError) {
throw e;
} else {
logger.debug("Unable to convert value '" + text
+ "' but ignoreError is true - ignoring error", e);
}
}
}
/**
* Convert an instance of {@link Properties} into an arbitrary object.
* @param properties the Properties to convert
* @return the object being edited
* @throws IllegalArgumentException if the object could not be materialised
* from the given properties
*/
protected abstract Object convertProperties(final Properties properties) throws IllegalArgumentException;
/**
* Helper method for safely accessing properties, throwing an {@link IllegalArgumentException}
* if the property does not exist.
* @param part the property to get
* @param properties the properties to access
* @return the value of the given property
* @throws IllegalArgumentException if the property did not exist
*/
protected final String getPropertyPart(final String part, final Properties properties) throws IllegalArgumentException {
if (properties.containsKey(part)
&& properties.getProperty(part) != null) {
return properties.getProperty(part);
}
if (ignoreError) {
logger.debug("Part '" + part + "' not present, ignoring.");
return null;
} else {
throw new IllegalArgumentException("Required part '" + part + "' not present.");
}
}
/**
* Helper method for safely accessing int properties, throwing an {@link IllegalArgumentException}
* if the property does not exist or is unparseable.
* @param part the property to get
* @param properties the properties to access
* @return the value of the given property
* @throws IllegalArgumentException if the property did not exist or is not an int
*/
protected final int getPropertyPartAsInt(final String part, final Properties properties) throws IllegalArgumentException {
try {
String propertyPart = getPropertyPart(part, properties);
return propertyPart == null ? 0 : Integer.parseInt(propertyPart);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Unparseable part '"
+ part + "' : '" + properties.getProperty(part) + "'");
}
}
/**
* Converts a {@link Properties} instance to a string representation.
* @param properties the properties to convert
* @return the string representation
*/
private String convertStringToProperties(final Properties properties) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
properties.store(out, null);
return new String(out.toByteArray());
} catch (IOException e) {
return e.getMessage();
}
}
}
The final part is a concrete implementation of the property editor for handling dates. This has a configurable strategy for handling several different combinations of date/time part parsing, but the default behaviour is to just parse dates:
Code:
package org.mooli.web.bind;
import java.util.Calendar;
import java.util.Date;
import java.util.Properties;
/**
* Editor that converts instances of {@link Properties} to {@link Date} objects.
* @author dhewitt
*/
public final class DateTimePropertyEditor extends AbstractCompositePropertyEditor {
/** Editor for setting the date portion. */
public static final DatePartEditor DATE_EDITOR = new DatePartEditor(
new String[] {"year", "month", "date"},
new int [] {Calendar.YEAR, Calendar.MONTH, Calendar.DATE}) {
/**
* @see org.mooli.web.bind.DateTimePropertyEditor.DatePartEditor
* #populateCalendar(java.util.Calendar, java.util.Properties, org.mooli.web.bind.DateTimePropertyEditor)
*/
public void populateCalendar(final Calendar cal, final Properties props, final DateTimePropertyEditor editor) {
String[] names = getNames();
int[] fields = getFields();
int year = editor.getPropertyPartAsInt(names[0], props);
if (year > 0) {
int month = editor.getPropertyPartAsInt(names[1], props);
int date = editor.getPropertyPartAsInt(names[2], props);
cal.set(year, month, 1);
int actualMin = cal.getActualMinimum(fields[2]);
int actualMax = cal.getActualMaximum(fields[2]);
int fixedDate = Math.max(actualMin, Math.min(date, actualMax));
cal.set(fields[2], fixedDate);
}
}
};
/** Editor for setting the hour. */
public static final DatePartEditor HOUR_EDITOR = new DatePartEditor("hour", Calendar.HOUR_OF_DAY);
/** Editor for setting the minute. */
public static final DatePartEditor MINUTE_EDITOR = new DatePartEditor("minute", Calendar.MINUTE);
/** Editor for setting the second. */
public static final DatePartEditor SECOND_EDITOR = new DatePartEditor("second", Calendar.SECOND);
/** The default editors used to convert {@link Properties} to {@link Date}s - only
* converts the date portion.
*/
public static final DatePartEditor[] DEFAULT_EDITORS = new DatePartEditor[] {
DATE_EDITOR};
/** The editors to use for binding date and time, excluding seconds. */
public static final DatePartEditor[] DATETIME_EDITORS = new DatePartEditor[] {
DATE_EDITOR, HOUR_EDITOR, MINUTE_EDITOR};
/** The editors to use for binding time, excluding seconds. */
public static final DatePartEditor[] TIME_EDITORS = new DatePartEditor[] {
HOUR_EDITOR, MINUTE_EDITOR};
/** The editors to use for binding date and time, including seconds. */
public static final DatePartEditor[] ALL_EDITORS = new DatePartEditor[] {
DATE_EDITOR, HOUR_EDITOR, MINUTE_EDITOR, SECOND_EDITOR};
/** The editors to use for extracting parts of a date. */
private DatePartEditor[] editors;
/**
* Create a property editor using the default date part editors (date only).
*/
public DateTimePropertyEditor() {
this(DEFAULT_EDITORS);
}
/**
* Create a property editor using the specified date part editors.
* @param newEditors the editors to use
*/
public DateTimePropertyEditor(final DatePartEditor[] newEditors) {
super();
this.editors = newEditors;
}
/**
* @param ignoreEmpty whether to ignore empty properties
*/
public DateTimePropertyEditor(final boolean ignoreEmpty) {
this(DEFAULT_EDITORS, ignoreEmpty);
}
/**
*
* @param newEditors the editors to use
* @param ignore whether to ignore empty properties
*/
public DateTimePropertyEditor(final DatePartEditor[] newEditors, final boolean ignore) {
super(ignore);
this.editors = newEditors;
}
/**
* @see org.mooli.web.bind.AbstractCompositePropertyEditor#getAsProperties()
*/
protected Properties getAsProperties() {
Calendar cal = Calendar.getInstance();
cal.setTime((Date)getValue());
Properties props = new Properties();
for (int i = 0; i < editors.length; i++) {
editors[i].populateProperties(cal, props);
}
return props;
}
/**
* @see org.mooli.web.bind.AbstractCompositePropertyEditor#convertProperties(java.util.Properties)
*/
protected Object convertProperties(final Properties properties) {
Calendar cal = Calendar.getInstance();
cal.clear();
for (int i = 0; i < editors.length; i++) {
editors[i].populateCalendar(cal, properties, this);
}
return cal.getTime();
}
/**
* Class for populating part of a date from values specified in a
* {@link Properties} object.
*/
private static class DatePartEditor {
/** The field names to use when getting keys from the {@link Properties} instance. */
private final String[] names;
/** The {@link Calendar} fields to populate. */
private final int[] fields;
/**
* @param name the name to use
* @param field the field to set
*/
public DatePartEditor(final String name, final int field) {
this(new String[] {name}, new int[] {field});
}
/**
* @param newNames the names to use
* @param newFields the corresponding fields to set
*/
public DatePartEditor(final String[] newNames, final int[] newFields) {
super();
this.names = newNames;
this.fields = newFields;
//assert names.length == fields.length;
}
/**
* Populate a {@link Calendar} based on an instance of {@link Properties}.
* @param cal the calendar to populate
* @param props the properties to use
* @param editor the enclosing editor
*/
public void populateCalendar(final Calendar cal, final Properties props, final DateTimePropertyEditor editor) {
for (int i = 0; i < names.length && i < fields.length; i++) {
cal.set(fields[i], editor.getPropertyPartAsInt(names[i], props));
}
}
/**
* Populate a {@link Properties} based on an instance of {@link Calendar}.
* @param cal the calendar to use
* @param props the properties to populate
*/
public final void populateProperties(final Calendar cal, final Properties props) {
for (int i = 0; i < names.length && i < fields.length; i++) {
props.setProperty(names[i], String.valueOf(cal.get(fields[i])));
}
}
/**
* @return Returns the fields.
*/
public final int[] getFields() {
return fields;
}
/**
* @return Returns the names.
*/
public final String[] getNames() {
return names;
}
}
}
This could all be tidied up somewhat, but the basic mechanism works well for me, and I have used this approach several times for other complex objects.