5.6 Spring 3 UI Field Formatting

core.convert is a simple, general-purpose type conversion system. It addresses one-way conversion from one type to another, and is not limited to just converting Strings. As discussed in the previous section, a Spring Container can be configured to use this system when binding bean property values. In addition, the Spring Expression Language (SpEL) uses this system to coerce Expression values. For example, when SpEL needs to coerse a Short to a Long to fullfill a expression.setValue attempt, the core.convert system performs the coersion.

Now consider the type conversion requirements of a typical UI environment such as a web or desktop application. In such environments, you typically convert from String to support the form postback process, as well as back to String to support the rendering process. The more general core.convert system does not address this specific scenario directly. To directly address this, Spring 3 introduces a new ui.format system that provides a simple and robust alternative to PropertyEditors in a UI environment.

In general, use Converters when you need implement general-purpose type conversion logic; logic that may be invoked by the Spring Container, SpEL, or your own code as part of a one-way binding process. Use Formatters when you're working in a UI environment such as a HTML form of a web application, and need to apply two-way parsing, formatting, and localization logic to form field values.

5.6.1 Formatter SPI

The Formatter SPI to implement UI formatting logic is simple and strongly typed:

package org.springframework.ui.format;

import java.text.ParseException;

public interface Formatter<T> {

    String format(T object, Locale locale);
	
    T parse(String formatted, Locale locale) throws ParseException;
    
}
			

To create your own Formatter, simply implement the interface above. Parameterize T to be the type of Object you are formatting; for example, java.lang.BigDecimal. Implement the format operation to format an instance of T for display in the client locale. Implement the parse operation to parse an instance of T from the formatted representation returned from the client locale. Your Formatter should throw a ParseException if a parse attempt fails. Take care to ensure your Formatter implementation is thread safe.

Several Formatter implementations are provided in subpackages of ui.format as a convenience. The date package provides a DateFormatter to format java.util.Date objects with a java.text.DateFormat. The number package provides a DecimalFormatter, IntegerFormatter, CurrencyFormatter, and PercentFormatter to format java.lang.Number objects using a java.text.NumberFormat.

Note DateFormatter as an example Formatter implementation:

package org.springframework.ui.format.date;

public final class DateFormatter implements Formatter<Date> {

    private String pattern;
    
    public DateFormatter(String pattern) {
        this.pattern = pattern;
    }
    
    public String format(Date date, Locale locale) {
        if (date == null) {
            return "";
        }
        return getDateFormat(locale).format(date);
    }

    public Date parse(String formatted, Locale locale) throws ParseException {
        if (formatted.length() == 0) {
            return null;
        }
        return getDateFormat(locale).parse(formatted);
    }

    protected DateFormat getDateFormat(Locale locale) {
        DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale);
        dateFormat.setLenient(false);
        return dateFormat;
    }

}
			

The Spring team welcomes community-driven Formatter contributions; see http://jira.springframework.org to contribute. In particular, the team hopes to integrate support for Joda Time and Money Formatters in the future.

5.6.2 @Formatted

The @Formatted annotation allows you to easily associate a Formatter implementation with one of your classes. To use this feature, simply annotate your class as @Formatted and specify the Formatter implementation to use as the annotation value:

@Formatted(MoneyFormatter.class)
public class Money {
    ...
}

			

The example above says "Money objects should be formatted by a MoneyFormatter". With this configuation, whenever a field is of type Money, MoneyFormatter will format the field value.

5.6.3 Custom Format Annotations

Field-specific formatting can be triggered by annotating model properties. To bind a custom annotation to a Formatter instance, simply annotate the annotation as @Formatted:

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Formatted(CurrencyFormatter.class)
public @interface Currency {
}			

			

Then, to trigger formatting, simply annotate a model property with the annotation:

public class MyModel {

    @Currency
    private BigDecimal amount;
	
}

			

Custom annotations like @Currency can also be annotated with JSR-303 constraint annotations to specify declarative validation constraints. For example:

@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Formatted(CurrencyFormatter.class)
@Constraint(validatedBy = CurrencyValidator.class)
public @interface Currency {
}			

			

Given the example above, on form postback any @Currency properties will first be parsed by CurrencyFormatter, then validated by CurrencyValidator.

5.6.3.1 AnnotationFormatterFactory

If your custom annotation has attributes that configure Formatter instance behavior by property, implement a AnnotationFormatterFactory:

package org.springframework.ui.format;

public interface AnnotationFormatterFactory<A extends Annotation, T> {

    Formatter<T> getFormatter(A annotation);

}			

				

The example implementation below binds a @DecimalFormat instance to a Formatter instance. This particular annotation allows the NumberFormat pattern to be configured.

public class DecimalAnnotationFormatterFactory implements AnnotationFormatterFactory<DecimalFormat, Number> {

    Formatter<Number> getFormatter(DecimalFormat annotation) {
        DecimalFormatter formatter = DecimalFormatter();
        formatter.setPattern(annotation.value());
        return formatter;
    }
}

				

Then, to trigger, simply annotate a property as a @DecimalFormat in your model:

public class MyModel {

    @DecimalFormat("#,###")
    private BigDecimal decimal;
	
}

				

5.6.4 FormatterRegistry SPI

Formatters can be registered in a FormatterRegistry. A DataBinder uses this registry to resolve the Formatter to use for a specific field. This allows you to configure default Formatting rules centrally, rather than duplicating such configuration across your UI Controllers. For example, you might want to enforce that all Date fields are formatted a certain way, or fields with a specific annotation are formatted in a certain way. With a shared FormatterRegistry, you define these rules once and they are applied whenever formatting is needed.

Review the FormatterRegistry SPI below:

package org.springframework.ui.format;

public interface FormatterRegistry {

    void add(Formatter<?> formatter);

    void add(AnnotationFormatterFactory<?, ?> factory);
    
}
			

As shown above, Formatters may be registered by field type or annotation. GenericFormatterRegistry is the implementation suitable for use in most UI binding environments. This implementation may be configured programatically or declaratively as a Spring bean.

5.6.5 Configuring a FormatterRegistry

A FormatterRegistry is a stateless object designed to be instantiated at application startup, then shared between multiple threads. In a Spring MVC application, you configure a FormatterRegistry as a property of the WebBindingInitializer. The FormatterRegistry will then be configured whenever a DataBinder is created by Spring MVC to bind and render model properties. If no FormatterRegistry is configured, the original PropertyEditor-based system is used.

To register a FormatterRegistry with Spring MVC, simply configure it as a property of a custom WebBindingInitializer injected into the Spring MVC AnnotationMethodHandlerAdapter:

<!-- Invokes Spring MVC @Controller methods -->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="webBindingInitializer">
        <!-- Configures Spring MVC DataBinder instances -->
        <bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
            <property name="formatterRegistry">
                <bean class="org.springframework.ui.format.support.GenericFormatterRegistry">
                    <property name="formatters">
                        <list>
	                        <!-- Register Formatter beans here -->
                        </list>	
                    </property>
                    <property name="annotationFormatterFactories">
                        <list>
	                        <!-- Register any AnnotationFormatterFactory beans here -->
                        </list>	
                    </property>
                </bean>
            </property>
        </bean>
    </property>
</bean>
			

When using the @Formatted annotation, no explicit Formatter or AnnotationFormatterFactory registration is required. See the JavaDocs for GenericFormatterRegistry for more configuration options.

5.6.6 Registering field-specific Formatters

In most cases, configuring a shared FormatterRegistry that selects Formatters based on model property type or annotation is sufficient. When sufficient, no special @Controller @InitBinder callbacks are needed to apply custom formatting logic. However, there are cases where field-specific formatting should be configured on a Controller-by-Controller basis. For example, you may need to format a specific Date field in a way that differs from all the other Date fields in your application.

To apply a Formatter to a single field, create an @InitBinder callback on your @Controller, then call binder.registerFormatter(String, Formatter):

    @Controller
    public class MyController {
    
        @InitBinder
        public void initBinder(WebDataBinder binder) {
            binder.registerFormatter("myFieldName", new MyCustomFieldFormatter());
        }
        
        ...
    }

			

This applies the Formatter to the field, and overrides any Formatter that would have been applied by field type or annotation.