From b56a47da97dcc1e5e5faf650024ad21e371f7cce Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Wed, 11 Nov 2009 18:53:02 +0000 Subject: [PATCH] annotation driven number formatting with default number formatting rules --- .../format/annotation/NumberFormat.java | 80 ++++++++ .../number/AbstractNumberFormatter.java | 2 - .../format/number/IntegerFormatter.java | 39 ---- ...malFormatter.java => NumberFormatter.java} | 11 +- ...ormattingConversionServiceFactoryBean.java | 8 + .../format/number/IntegerFormatterTests.java | 54 ------ ...erTests.java => NumberFormatterTests.java} | 6 +- .../format/number/NumberFormattingTests.java | 136 ++++++++++++++ ...tingConversionServiceFactoryBeanTests.java | 43 +++++ .../FormattingConversionServiceTests.java | 4 +- .../validation/DataBinderTests.java | 4 +- spring-framework-reference/src/validation.xml | 173 ++++++------------ 12 files changed, 335 insertions(+), 225 deletions(-) create mode 100644 org.springframework.context/src/main/java/org/springframework/format/annotation/NumberFormat.java delete mode 100644 org.springframework.context/src/main/java/org/springframework/format/number/IntegerFormatter.java rename org.springframework.context/src/main/java/org/springframework/format/number/{DecimalFormatter.java => NumberFormatter.java} (90%) delete mode 100644 org.springframework.context/src/test/java/org/springframework/format/number/IntegerFormatterTests.java rename org.springframework.context/src/test/java/org/springframework/format/number/{DecimalFormatterTests.java => NumberFormatterTests.java} (89%) create mode 100644 org.springframework.context/src/test/java/org/springframework/format/number/NumberFormattingTests.java create mode 100644 org.springframework.context/src/test/java/org/springframework/format/support/FormattingConversionServiceFactoryBeanTests.java diff --git a/org.springframework.context/src/main/java/org/springframework/format/annotation/NumberFormat.java b/org.springframework.context/src/main/java/org/springframework/format/annotation/NumberFormat.java new file mode 100644 index 00000000000..adb3d10b210 --- /dev/null +++ b/org.springframework.context/src/main/java/org/springframework/format/annotation/NumberFormat.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2009 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.format.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares that a field should be formatted as a number. + * Supports formatting by style or custom pattern string. + * Can be applied to any JDK java.lang.Number type. + *

+ * For style-based formatting, set the {@link #style()} attribute to be the desired {@link Style}. + * For custom formatting, set the {@link #pattern()} attribute to be the number pattern, such as #,###.##. + *

+ * Each attribute is mutually exclusive, so only set one attribute per annotation instance (the one most convenient one for your formatting needs). + * When the pattern attribute is specified, it takes precedence over the style attribute. + * When no annotation attributes are specified, the default format applied is style-based with a style of {@link Style#NUMBER}. + * + * @author Keith Donald + * @since 3.0 + * @see org.joda.time.format.DateTimeFormat + */ +@Target( { ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface NumberFormat { + + /** + * The style pattern to use to format the field. + * Defaults to {@link Style#NUMBER} for general-purpose number formatter. + * Set this attribute when you wish to format your field in accordance with a common style other than the default style. + */ + Style style() default Style.NUMBER; + + /** + * The custom pattern to use to format the field. + * Defaults to empty String, indicating no custom pattern String has been specified. + * Set this attribute when you wish to format your field in accordance with a custom number pattern not represented by a style. + */ + String pattern() default ""; + + /** + * Common number format styles. + * @author Keith Donald + * @since 3.0 + */ + public enum Style { + + /** + * General-purpose number format for the current locale. + */ + NUMBER, + + /** + * A currency format for the current locale. + */ + CURRENCY, + + /** + * A percent format for the current locale. + */ + PERCENT + + } +} diff --git a/org.springframework.context/src/main/java/org/springframework/format/number/AbstractNumberFormatter.java b/org.springframework.context/src/main/java/org/springframework/format/number/AbstractNumberFormatter.java index 2b361ff9967..69644f9d308 100644 --- a/org.springframework.context/src/main/java/org/springframework/format/number/AbstractNumberFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/format/number/AbstractNumberFormatter.java @@ -35,7 +35,6 @@ public abstract class AbstractNumberFormatter implements Formatter { private boolean lenient = false; - /** * Specify whether or not parsing is to be lenient. Default is false. *

With lenient parsing, the parser may allow inputs that do not precisely match the format. @@ -45,7 +44,6 @@ public abstract class AbstractNumberFormatter implements Formatter { this.lenient = lenient; } - public String print(Number integer, Locale locale) { return getNumberFormat(locale).format(integer); } diff --git a/org.springframework.context/src/main/java/org/springframework/format/number/IntegerFormatter.java b/org.springframework.context/src/main/java/org/springframework/format/number/IntegerFormatter.java deleted file mode 100644 index d1840d7a161..00000000000 --- a/org.springframework.context/src/main/java/org/springframework/format/number/IntegerFormatter.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2002-2009 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.format.number; - -import java.text.NumberFormat; -import java.util.Locale; - -/** - * A Number formatter for whole integer values. - * - *

Delegates to {@link NumberFormat#getIntegerInstance(Locale)}. - * The {@link #parse(String, Locale)} routine always returns a Long. - * - * @author Keith Donald - * @author Juergen Hoeller - * @since 3.0 - * @see #setLenient - */ -public final class IntegerFormatter extends AbstractNumberFormatter { - - protected NumberFormat getNumberFormat(Locale locale) { - return NumberFormat.getIntegerInstance(locale); - } - -} diff --git a/org.springframework.context/src/main/java/org/springframework/format/number/DecimalFormatter.java b/org.springframework.context/src/main/java/org/springframework/format/number/NumberFormatter.java similarity index 90% rename from org.springframework.context/src/main/java/org/springframework/format/number/DecimalFormatter.java rename to org.springframework.context/src/main/java/org/springframework/format/number/NumberFormatter.java index cb3fd391e56..d373afc848e 100644 --- a/org.springframework.context/src/main/java/org/springframework/format/number/DecimalFormatter.java +++ b/org.springframework.context/src/main/java/org/springframework/format/number/NumberFormatter.java @@ -21,7 +21,7 @@ import java.text.NumberFormat; import java.util.Locale; /** - * A Number formatter for decimal values. + * A general-purpose Number formatter. * *

Delegates to {@link NumberFormat#getInstance(Locale)}. * Configures BigDecimal parsing so there is no loss in precision. @@ -34,10 +34,17 @@ import java.util.Locale; * @see #setPattern * @see #setLenient */ -public final class DecimalFormatter extends AbstractNumberFormatter { +public final class NumberFormatter extends AbstractNumberFormatter { private String pattern; + public NumberFormatter() { + + } + + public NumberFormatter(String pattern) { + this.pattern = pattern; + } /** * Sets the pattern to use to format number values. diff --git a/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java b/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java index 555ec84ec3a..2134a0929d4 100644 --- a/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java +++ b/org.springframework.context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java @@ -19,6 +19,8 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.convert.ConversionService; import org.springframework.format.datetime.joda.JodaTimeFormattingConfigurer; +import org.springframework.format.number.NumberFormatAnnotationFormatterFactory; +import org.springframework.format.number.NumberFormatter; import org.springframework.util.ClassUtils; /** @@ -40,6 +42,7 @@ public class FormattingConversionServiceFactoryBean implements FactoryBean - Validation, Data-binding, the <interfacename>BeanWrapper</interfacename>, and <literal>PropertyEditors</literal> + Validation, Data Binding, and Type Conversion

Introduction @@ -345,7 +345,7 @@ Float salary = (Float) company.getPropertyValue("managingDirector.salary");]]> Built-in <interface>PropertyEditor</interface> implementations - Spring heavily uses the concept of PropertyEditors to effect the conversion + Spring uses the concept of PropertyEditors to effect the conversion between an Object and a String. If you think about it, it sometimes might be handy to be able to represent properties in a different way than the object itself. For example, a Date can be represented in a human readable way (as the @@ -910,65 +910,64 @@ public class MyService {
- Spring 3 UI Field Formatting + Spring 3 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. + It provides a strongly-typed Converter SPI for implementing one-way conversion logic 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 to bind bean property values. - In addition, the Spring Expression Language (SpEL) uses this system to coerce Expression values. - For example, when SpEL needs to coerce a Short to a Long to fulfill an - expression.setValue() attempt, the core.convert system performs the coercion. + In addition, both the Spring Expression Language (SpEL) and DataBinder can use this system to coerce values. + For example, when SpEL needs to coerce a Short to a Long to complete an expression.setValue(Object bean, Object value) attempt, the core.convert system performs the coercion. - 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. + Now consider the type conversion requirements of a typical client environment such as a web or desktop application. + In such environments, you typically convert from String to support the client postback process, as well as back to String to support the rendering process. + The more general core.convert Converter SPI does not address this specific common scenario directly. + To directly address this, Spring 3 introduces a conveient format SPI that provides a simple and robust alternative to PropertyEditors in a client environment. - In general, use Converters when you need to 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 an HTML form - of a web application and need to apply two-way parsing, - formatting, and localization logic to form field values. + In general, use the Converter SPI when you need to implement general-purpose type conversion logic. + Use Formatters when you're working in a client environment, such as an HTML form of a web application and need to apply String parsing, printing, and localization logic to form field values.
Formatter SPI - The Formatter SPI to implement UI formatting logic is simple and strongly typed: + The org.springframework.format SPI to implement field formatting logic is simple and strongly typed: { + String print(T fieldValue, Locale locale); +}]]> + + { - - String format(T object, Locale locale); - - T parse(String formatted, Locale locale) throws ParseException; - -}]]> +public interface Parser { + T parse(String clientValue, Locale locale) throws ParseException; +}]]> + + extends Printer, Parser { +}]]> + - To create your own Formatter, simply implement the interface above. + To create your own Formatter, simply implement the Formatter 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 print operation to print 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. + Your Formatter should throw a ParseException or IllegalArgumentException 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. + Several Formatter implementations are provided in formatsubpackages as a convenience. + The datetime 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. - The jodatime package provides a DateTimeFormatter to format Joda DateTime objects, a popular alternative to java.util.Date/Calendar. + The datetime.joda package provides comprehensive datetime formatting support based on the Joda Time library. Consider DateFormatter as an example Formatter implementation: { @@ -978,7 +977,7 @@ public final class DateFormatter implements Formatter { this.pattern = pattern; } - public String format(Date date, Locale locale) { + public String print(Date date, Locale locale) { if (date == null) { return ""; } @@ -1003,72 +1002,31 @@ public final class DateFormatter implements Formatter { The Spring team welcomes community-driven Formatter contributions; see http://jira.springframework.org to contribute.
-
- @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: - - - - 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. - -
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: + Field formatting can be triggered by annotating model properties. + To bind a custom annotation to a Formatter instance, implement AnnotationFormatterFactory: - - - Then, to trigger formatting, simply annotate a model property with the annotation: - - - - Custom annotations like @Currency can also be annotated with JSR-303 constraint annotations to specify declarative validation constraints. - For example: - - - - Given the example above, on form postback any @Currency properties will first be parsed by CurrencyFormatter, then validated by CurrencyValidator. - -
- AnnotationFormatterFactory - - If your custom annotation has attributes that configure Formatter - instance behavior by property, implement an AnnotationFormatterFactory: - { +public interface AnnotationFormatterFactory { - Formatter getFormatter(A annotation); + Set> getFieldTypes(); + + Printer getPrinter(A annotation, Class fieldType); + + Parser getParser(A annotation, Class fieldType); }]]> + + Parameterize A to be the field annotationType you wish to associate formatting logic with, for example org.springframework.format.annotation.DateTimeFormat. + Implement the getFieldTypes operation return the types of fields the annotation may be used on. + Implement the getPrinter operation to return the Printer to print the value of an annotated field. + Implement the getParser operation to return the Parser to parse the value of an annotated field. + Take care to ensure your AnnotationFormatterFactory implementation is thread-safe. + The example implementation below binds a @DecimalFormat instance to a Formatter instance. This particular annotation allows the NumberFormat pattern to be configured. @@ -1107,7 +1065,7 @@ public class MyModel { Review the FormatterRegistry SPI below: - + @@ -1167,33 +1125,6 @@ public interface FormatterRegistry { See the JavaDocs for GenericFormatterRegistry for more configuration options.
-
- 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): - - - - This applies the Formatter to the field and overrides any Formatter - that would have been applied by field type or annotation. - -
Spring 3 Validation