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

Reply via email to