This was my first day of work after a long holiday break, and I finally got something reasonably clean that I can share. To recap, my goal was to create a generic solution for allowing precise control of the number format used in a TextField that can be customized per field instance. The approach I came up with is to create a special binding prefix that interprets the binding expression as a number pattern string (a non-localized pattern string as per DecimalFormat). The result is that I can now do things like
<t:textField t:id="latitude" value="worksite.latitude" translate="numberFormat:0.00000" /> and <t:textField t:id="amount" value="card.amount" translate="numberFormat:#,##0.00" /> with no custom code required in the page class, and the error messages can be overridden in the standard fashion if desired. The solution consists of 3 new classes: NumberFormatBindingFactory, NumberFormatBinding, and NumberTranslator. Here is the code: ------------------------------------------------------------------------ public class NumberFormatBindingFactory implements BindingFactory { private final FieldTranslatorSource fieldTranslatorSource; private final TypeCoercer typeCoercer; public NumberFormatBindingFactory( FieldTranslatorSource fieldTranslatorSource, TypeCoercer typeCoercer ) { this.fieldTranslatorSource = fieldTranslatorSource; this.typeCoercer = typeCoercer; } public Binding newBinding( String description, ComponentResources containerResources, ComponentResources fieldResources, String expression, Location location ) { return new NumberFormatBinding( fieldResources, expression, location, fieldTranslatorSource, typeCoercer ); } } ------------------------------------------------------------------------ public class NumberFormatBinding extends BaseLocatable implements Binding { private final Translator<?> translator; private final FieldTranslatorSource fieldTranslatorSource; private final Field field; private final String overrideId; private final Messages overrideMessages; private final Locale locale; private FieldTranslator<?> fieldTranslator; public NumberFormatBinding( ComponentResources fieldResources, String formatPattern, Location location, FieldTranslatorSource fieldTranslatorSource, TypeCoercer typeCoercer ) { super( location ); this.translator = createTranslator( fieldResources, formatPattern, typeCoercer ); this.fieldTranslatorSource = fieldTranslatorSource; this.field = (Field) fieldResources.getComponent(); this.overrideId = fieldResources.getId(); this.overrideMessages = fieldResources.getContainerMessages(); this.locale = fieldResources.getLocale(); } public Object get() { if (fieldTranslator == null) { // Create the FieldTranslator lazily to ensure FormSupport is present. fieldTranslator = fieldTranslatorSource.createTranslator( field, overrideId, overrideMessages, locale, translator ); } return fieldTranslator; } public void set( Object value ) { throw new UnsupportedOperationException(); } public Class<?> getBindingType() { return get().getClass(); } public boolean isInvariant() { return true; } public <A extends Annotation> A getAnnotation( Class<A> annotationClass ) { return null; } private NumberFormat createNumberFormat( String pattern, Locale locale ) { DecimalFormatSymbols symbols = new DecimalFormatSymbols( locale ); DecimalFormat format = new DecimalFormat( pattern, symbols ); if (pattern.indexOf('.') < 0) { format.setParseIntegerOnly( true ); } return format; } @SuppressWarnings("unchecked") private <T extends Number> Translator<T> createTranslator( ComponentResources fieldResources, String formatPattern, TypeCoercer typeCoercer ) { NumberFormat numberFormat = createNumberFormat( formatPattern, fieldResources.getLocale() ); Class<T> numberType = fieldResources.getBoundType("value"); if (numberType == null) { throw new IllegalStateException("'value' parameter not bound for " + fieldResources.getId() +"; numeric type unknown."); } return new NumberTranslator<T>( numberType, numberFormat, typeCoercer ); } } ------------------------------------------------------------------------ public class NumberTranslator<T extends Number> implements Translator<T> { private static final List<Class<?>> INTEGER_TYPES = Arrays.asList( new Class<?>[] { Byte.class, Short.class, Integer.class, Long.class, BigInteger.class }); private final Class<T> type; private final NumberFormat formatter; private final TypeCoercer typeCoercer; private final String messageKey; public NumberTranslator( Class<T> type, NumberFormat formatter, TypeCoercer typeCoercer ) { this.type = type; this.formatter = formatter; this.typeCoercer = typeCoercer; this.messageKey = INTEGER_TYPES.contains(type) ? "integer-format-exception" : "number-format-exception"; } public String getName() { return "number"; } public Class<T> getType() { return type; } public String getMessageKey() { return messageKey; } public T parseClient( Field field, String value, String message ) throws ValidationException { if (value == null || value.length() == 0) { return null; } ParsePosition pp = new ParsePosition( 0 ); Number number = formatter.parse( value, pp ); // All input characters must be consumed to constitute success. if ((number != null) && (pp.getIndex() != value.length())) { number = null; } if (number == null) { throw new ValidationException( message ); } return typeCoercer.coerce( number, type ); } public String toClient( T value ) { return formatter.format( value ); } public void render( Field field, String message, MarkupWriter writer, FormSupport formSupport ) { // empty; no client-side support } } ------------------------------------------------------------------------ // and in AppModule... public static void contributeBindingSource( MappedConfiguration<String,BindingFactory> configuration, FieldTranslatorSource fieldTranslatorSource, TypeCoercer typeCoercer ) { configuration.add("numberFormat", new NumberFormatBindingFactory( fieldTranslatorSource, typeCoercer )); } ------------------------------------------------------------------------ I really didn't expect to have to spend 2 days on this, and I am sure it's still not perfect. I thought this was a basic requirement present in many applications and expected built-in support for it (or at least an easy add-on). Anyway, if anyone can improve on this, please let me know. I hope future releases of Tapestry will address this need. Regards, Benny